diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..19df53a --- /dev/null +++ b/.distignore @@ -0,0 +1,50 @@ +# Files and directories excluded from the distributable plugin archive. +# This file documents what does NOT ship to end users. + +# Version control +.git +.gitignore +.gitkeep + +# CI / development infrastructure +.github +.dev +.claude +.idea +.vscode +.worktrees +.dockerignore +.phpunit.result.cache +.DS_Store + +# Documentation (readme.txt is the WP standard) +docs +CONTRIBUTORS.md +AGENTS.md +CLAUDE.md +README.md + +# Build tooling +bin +Makefile +composer.lock +phpcs.xml +phpunit.xml.dist + +# Node.js / Playwright (e2e only) +node_modules +package.json +package-lock.json +tsconfig.json +playwright.config.ts +test-results +playwright-report + +# Build config (source and compiled blocks both ship) +webpack.config.js + +# Build output +builds + +# Tests +tests diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f679332 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,60 @@ +name: Bug Report +description: Something isn't working as expected +labels: [bug, triage] + +body: + - type: input + id: plugin-version + attributes: + label: Plugin version + placeholder: "0.2.4" + validations: + required: true + + - type: input + id: wp-version + attributes: + label: WordPress version + placeholder: "6.8" + validations: + required: true + + - type: input + id: php-version + attributes: + label: PHP version + placeholder: "8.3" + validations: + required: true + + - type: textarea + id: description + attributes: + label: What happened? + description: A clear description of the bug. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Go to '...' + 2. Click '...' + 3. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant logs or screenshots + description: Paste any error messages, stack traces, or screenshots. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ed9f7f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false + +contact_links: + - name: WordPress Support Forum + url: https://wordpress.org/support/plugin/airo-wp/ + about: Ask questions and get community support diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..0de88d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: [enhancement] + +body: + - type: textarea + id: problem + attributes: + label: What problem does this solve? + description: A clear description of the problem or use case driving this request. + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: Describe what you'd like to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Any other approaches you've thought about and why you ruled them out. + + - type: textarea + id: context + attributes: + label: Additional context + description: Mockups, references, or anything else that helps. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ddda31 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: CI + +# PR gate: all four jobs run natively on ubuntu-24.04 — no Docker, no make. +# PHP 7.4 / WP 7.0 on PRs guards the plugin's hard PHP 7.4 compatibility requirement. + +on: + pull_request: + branches: [main, 'feature/**', 'fix/**'] + push: + branches: [main] + +permissions: + contents: read + pull-requests: write + +jobs: + lint: + name: PHPCS + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Set up toolchain + run: sh scripts/setup/unit.sh + env: + PHP_VERSION: '7.4' + - name: Run PHPCS + run: sh scripts/tests/lint.sh + + unit: + name: PHPUnit + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Set up toolchain + run: sh scripts/setup/unit.sh + env: + PHP_VERSION: '7.4' + - name: Run PHPUnit + run: sh scripts/tests/unit.sh + + test-e2e: + name: Functional E2E + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Set up e2e environment + run: sh scripts/setup/e2e.sh + env: + PHP_VERSION: '7.4' + WP_VERSION: '7.0' + - name: Run functional e2e suite + run: sh scripts/tests/e2e.sh + env: + PHP_VERSION: '7.4' + WP_VERSION: '7.0' + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 7 + + check-plugin: + name: Plugin Check (PCP) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Set up e2e environment + run: sh scripts/setup/e2e.sh + env: + PHP_VERSION: '7.4' + WP_VERSION: '7.0' + - name: Run Plugin Check suite + run: sh scripts/tests/plugin-check.sh + env: + PHP_VERSION: '7.4' + WP_VERSION: '7.0' + - name: Post Plugin Check results as PR comment + if: always() && github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2 + with: + path: builds/plugin-check-results.txt + - name: Upload Plugin Check results on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: plugin-check-results + path: builds/plugin-check-results.txt + retention-days: 7 diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000..78fc2cb --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,118 @@ +name: Pre-release + +# Full PHP × WP matrix sweep. +# Triggers automatically on release/* PRs to main; also runnable manually. + +on: + workflow_dispatch: + inputs: + php_version: + description: 'PHP version to test, or "all"' + default: 'all' + wp_version: + description: 'WP version to test, or "all"' + default: 'all' + pull_request: + branches: [main] + +jobs: + matrix-setup: + name: Build matrix + runs-on: ubuntu-24.04 + if: startsWith(github.head_ref, 'release/') || github.event_name == 'workflow_dispatch' + outputs: + php: ${{ steps.set.outputs.php }} + wp: ${{ steps.set.outputs.wp }} + steps: + - id: set + run: | + if [ -z "${{ inputs.php_version }}" ] || [ "${{ inputs.php_version }}" = "all" ]; then + echo 'php=["7.4","8.0","8.1","8.2","8.3"]' >> $GITHUB_OUTPUT + else + echo 'php=["${{ inputs.php_version }}"]' >> $GITHUB_OUTPUT + fi + if [ -z "${{ inputs.wp_version }}" ] || [ "${{ inputs.wp_version }}" = "all" ]; then + echo 'wp=["6.8","6.9","7.0"]' >> $GITHUB_OUTPUT + else + echo 'wp=["${{ inputs.wp_version }}"]' >> $GITHUB_OUTPUT + fi + + lint: + name: PHPCS (PHP 8.3) + runs-on: ubuntu-24.04 + if: startsWith(github.head_ref, 'release/') || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + - run: sh scripts/setup/unit.sh + env: + PHP_VERSION: '8.3' + - run: sh scripts/tests/lint.sh + + unit: + name: PHPUnit (PHP ${{ matrix.php }}) + needs: matrix-setup + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: ${{ fromJson(needs.matrix-setup.outputs.php) }} + steps: + - uses: actions/checkout@v4 + - run: sh scripts/setup/unit.sh + env: + PHP_VERSION: ${{ matrix.php }} + - run: sh scripts/tests/unit.sh + + test-e2e: + name: E2E (PHP ${{ matrix.php }} × WP ${{ matrix.wp }}) + needs: matrix-setup + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: ${{ fromJson(needs.matrix-setup.outputs.php) }} + wp: ${{ fromJson(needs.matrix-setup.outputs.wp) }} + steps: + - uses: actions/checkout@v4 + - run: sh scripts/setup/e2e.sh + env: + PHP_VERSION: ${{ matrix.php }} + WP_VERSION: ${{ matrix.wp }} + - run: sh scripts/tests/e2e.sh + env: + PHP_VERSION: ${{ matrix.php }} + WP_VERSION: ${{ matrix.wp }} + - if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-php${{ matrix.php }}-wp${{ matrix.wp }} + path: | + playwright-report/ + test-results/ + retention-days: 7 + + check-plugin: + name: Plugin Check (PHP ${{ matrix.php }} × WP ${{ matrix.wp }}) + needs: matrix-setup + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: ${{ fromJson(needs.matrix-setup.outputs.php) }} + wp: ${{ fromJson(needs.matrix-setup.outputs.wp) }} + steps: + - uses: actions/checkout@v4 + - run: sh scripts/setup/e2e.sh + env: + PHP_VERSION: ${{ matrix.php }} + WP_VERSION: ${{ matrix.wp }} + - run: sh scripts/tests/plugin-check.sh + env: + PHP_VERSION: ${{ matrix.php }} + WP_VERSION: ${{ matrix.wp }} + - if: failure() + uses: actions/upload-artifact@v4 + with: + name: plugin-check-php${{ matrix.php }}-wp${{ matrix.wp }} + path: builds/plugin-check-results.txt + retention-days: 7 diff --git a/.github/workflows/publish-plugin.yml b/.github/workflows/publish-plugin.yml new file mode 100644 index 0000000..cca9abb --- /dev/null +++ b/.github/workflows/publish-plugin.yml @@ -0,0 +1,14 @@ +name: Publish Plugin + +on: + workflow_dispatch: + push: + tags: ['v[0-9]+.[0-9]+.[0-9]+'] + +jobs: + publish: + name: Publish to WordPress.org + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f28b491 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +/docs/superpowers/ +/docs/specs/ +/docs/tool-migration/ +/.worktrees/ + +/.aws/ +/.cache/ + +/vendor/ +/bin/strauss.phar +/dependencies/ +/.phpunit.result.cache +/coverage/ +/.idea/ +/.vscode/ +.DS_Store + +/node_modules/ +/builds/ +/test-results/ +/playwright-report/ +/plugin-check-results/ + +# DSG-owned — generated by make dsg-sync, never edited directly and not committed. +# Regenerated by make dsg-sync; shipped to the zip via the package.json "files" +# allowlist (build-zip.sh runs sync + build), so they don't need to be tracked. +/artifacts/storage-states/*.json diff --git a/.puballow b/.puballow new file mode 100644 index 0000000..38f92f3 --- /dev/null +++ b/.puballow @@ -0,0 +1,66 @@ +# .puballow — rsync filter rules for the public mirror workflow +# +# ALLOWLIST: everything is private by default. +# Add a + rule to make a file or directory public. +# The trailing "- *" excludes anything not explicitly listed. +# +# Format (rsync filter rules): +# + pattern include — sync to public repo +# - pattern exclude — keep private +# First matching rule wins. +# +# For directories, two rules are required: +# + /dir/ lets rsync descend into the directory +# + /dir/** includes all contents recursively +# +# Public GitHub files live in .github/public/ in this repo. +# The sync workflow remaps them to .github/ in the public repo. +# Add workflows, issue templates, etc. under .github/public/ — no changes here needed. + +# ── Top-level files ───────────────────────────────────────────────────────── ++ /airo-wp.php ++ /readme.txt ++ /CHANGELOG.md ++ /README.md ++ /CONTRIBUTORS.md ++ /.gitignore ++ /.distignore ++ /.puballow ++ /composer.json ++ /composer.lock ++ /package.json ++ /package-lock.json ++ /phpcs.xml ++ /phpunit.xml.dist ++ /playwright.config.ts ++ /tsconfig.json ++ /webpack.config.js + +# ── PHP source ─────────────────────────────────────────────────────────────── ++ /includes/ ++ /includes/** ++ /functions/ ++ /functions/** + +# ── Tests ───────────────────────────────────────────────────────────────────── ++ /tests/ ++ /tests/** + +# ── Public GitHub files (.github/public/ → remapped to .github/ on sync) ──── +# Add workflows, issue templates, etc. under .github/public/ in this repo. ++ /.github/ ++ /.github/public/ ++ /.github/public/** + +# ── DSG-generated source (gitignored; populated by dsg-sync before rsync) ──── ++ /src/ ++ /src/** ++ /patterns/ ++ /patterns/** ++ /data/ ++ /data/** ++ /dist/ ++ /dist/** + +# ── Catch-all: exclude everything not listed above ──────────────────────────── +- * diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f49b09c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## 0.2.4 + +- Bundled block dynamic tags so shipped blocks resolve dynamic content correctly +- Corrected block class renames, docblocks, and patch paths in synced block source + +## 0.2.3 + +- Renamed plugin to "Airo WP AI Builder" (WordPress.org trademark compliance) +- Fixed PHP syntax error: ABSPATH guard now placed after namespace declaration +- Fixed inline style in form submission meta box to use wp_add_inline_style() +- Scrubbed remote image URLs (Unsplash, picsum) from block patterns and src/blocks +- Corrected Plugin URI and Description header in plugin file +- Added external services documentation for Cloudflare Turnstile and Google Maps + +## 0.2.2 + +- Fixed readme short description exceeding the 150-character limit + +## 0.2.1 + +- Renamed plugin display name to "Airo WP" + +## 0.2.0 + +- Added MCP server with tools for posts, pages, media, templates, navigation, and global styles +- Added DesignSetGo blocks and patterns with automated sync pipeline +- Added website management tools (site info, plugins, themes) +- Added release tooling with version bump, build pipeline, and distributable zip packaging +- E2e and plugin-check tests now validate the distributable build artifact diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..b61c46c --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,328 @@ +# Contributing to Airo WP + +Thank you for contributing to the Airo WordPress plugin. This document explains how to get started, how the project is organized, and what we expect in pull requests. + +## What this repository is + +Airo WP is a **plugin scaffold**: bootstrap, DI container, package loader, Strauss vendor isolation, tests, and CI. Feature work happens in **domain packages** under `includes/{Domain}/`, not in the root bootstrap file. + +## Prerequisites + +Before you begin, install: + +| Tool | Version | +|------|---------| +| PHP | 7.4+ (use **8.3** locally when possible) | +| Composer | 2.x | +| Git | 2.x | +| WordPress | 6.8+ (local site or container) | + +Optional but recommended: + +- A local WordPress environment (e.g. site-designer-local or your team's standard stack) +- `phpcs` / `phpcbf` via Composer (included in dev dependencies) + +## Getting started + +### 1. Clone and install dependencies + +```bash +git clone git@github.com:godaddy-wordpress/airo-wp.git +cd airo-wp +composer install +``` + +This installs dev tools (PHPUnit, Brain Monkey, WPCS, VIPWPCS), downloads the Strauss phar, and wires autoloaders. + +### 2. Link the plugin into WordPress + +Point your local site's plugins directory at this repo: + +```bash +# Example: symlink (adjust paths to your environment) +ln -s "$(pwd)" /path/to/wordpress/wp-content/plugins/airo-wp +``` + +Or copy the built plugin tree into `wp-content/plugins/airo-wp/`. + +### 3. Prefix runtime dependencies (when changing deps or autoload) + +```bash +composer build +``` + +Run this after adding or updating Composer `require` packages, or after changing PSR-4 paths. It runs Strauss to refresh prefixed packages under `dependencies/`, then regenerates `vendor/autoload.php` via Composer. The `dependencies/` directory is gitignored — do not commit it. + +### 4. Verify everything passes + +With Docker (recommended; caches `vendor` in named volumes): + +```bash +make check # test + lint (recommended before opening a PR) +make test PHP=8.0 # test on a specific version only +make lint # lint only +make docker-build-all # build all matrix images locally (optional) +``` + +CI runs the same commands automatically: +- `.github/workflows/phpunit.yml` — PHPUnit matrix on PHP 7.4–8.3 (PR + push to `main`) +- `.github/workflows/phpcs.yml` — PHPCS on PHP 8.3 (PR + push to `main`) +- `.github/workflows/plugin-check.yml` — WordPress Plugin Check on PHP 8.3, WP latest (PR + push to `main`) + +Or locally with Composer (single host PHP only): + +```bash +composer test +composer lint +``` + +Both must pass before you open a pull request. Build images once with `make docker-build` if `airo-wp-test` / `airo-wp-linter` tags are missing. + +### 5. Activate in WordPress + +In wp-admin → **Plugins**, activate **Airo WP**. The scaffold has no settings screen; activation only boots the container and placeholder `scaffold` package. + +## Development commands + +| Command | Description | +|---------|-------------| +| `make test` | PHPUnit in Docker (default PHP 8.3) | +| `make test PHP=8.0` | PHPUnit in Docker on a specific version (7.4–8.3) | +| `make lint` | PHPCS in Docker | +| `make check` | test + lint | +| `make docker-build` | Build `airo-wp-test` and `airo-wp-linter` images | +| `make docker-build-all` | Build test images for every PHP version | +| `make plugin-check` | Run WordPress Plugin Check in Docker (PHP 8.3, WP latest) | +| `make plugin-check PHP=8.3 WP=6.8` | Plugin Check against a specific WP version | +| `make docker-build-e2e` | Build `airo-wp-e2e` image (PHP + Node.js + WP-CLI) | +| `make e2e` | Run e2e tests in Docker (PHP 8.3, WP 6.9.4) | +| `make e2e PHP=8.0 WP=6.8` | Run e2e tests against a specific PHP × WP version | +| `make playground` | Run WP Playground in Docker (PHP 8.3, latest WP) | +| `make playground PHP=8.2 WP=6.8` | Playground with specific PHP × WP version | +| `make playground-stop` | Stop the running playground container | +| `make playground-reset` | Stop and remove all playground data (fresh start) | +| `make version-bump TYPE=patch` | Bump plugin version in all files (patch/minor/major) | +| `make package` | Build distributable zip (`builds/airo-wp.zip`) | +| `make build TYPE=patch` | Version bump + full build (patch/minor/major) | +| `composer test` | PHPUnit on host PHP (no version matrix) | +| `composer lint` | PHPCS — WPCS + VIP-Go + PHPCompatibility | +| `composer format` | PHPCBF auto-fix | +| `composer build` | Strauss-prefix runtime deps into `dependencies/` | + +## Plugin Check + +The [WordPress Plugin Check](https://wordpress.org/plugins/plugin-check/) (PCP) verifies the plugin satisfies WordPress.org requirements (security, performance, accessibility, plugin-review standards). + +### Running locally + +```bash +# Build the e2e image once (shared with e2e tests) +make docker-build-e2e + +# Run Plugin Check (default: PHP 8.3, WP latest) +make plugin-check + +# Run against a specific WP version +make plugin-check PHP=8.3 WP=6.8 +``` + +### How it works + +1. `tests/e2e/setup/plugin-check-entrypoint.sh` runs inside the container: + - Builds a distributable zip via `.dev/release/build-zip.sh` (Composer, DSG sync, wp-scripts build, plugin-zip) + - Downloads WordPress via WP-CLI and sets up SQLite database integration (no MySQL needed) + - Installs the plugin from the built zip (`wp plugin install builds/airo-wp-test.zip`) + - Installs the Plugin Check plugin and runs `wp plugin check` with `--require=cli.php` for runtime checks +2. Full results are saved as markdown to `plugin-check-results/full-results.txt` (posted as a sticky PR comment in CI) +3. A filtered run (excluding baseline ignore codes) determines pass/fail +4. The job hard-fails if any ERROR-type finding is detected in the filtered output + +### The `.distignore` file + +`.distignore` documents which files and directories are excluded from the distributable plugin archive. The plugin-check build step mirrors these exclusions. When adding new dev-only files or directories, add them to `.distignore` to keep them out of the build. + +## End-to-end tests + +E2e tests run WordPress via [`@wp-playground/cli`](https://github.com/WordPress/playground-tools/tree/trunk/packages/playground-cli) (WASM PHP + bundled SQLite, no separate database) and drive the REST API with [Playwright](https://playwright.dev). + +### Prerequisites + +- Docker (same requirement as unit tests) +- No local Node.js or PHP needed — everything runs inside the container + +### Running locally + +```bash +# Build the e2e image once (or after changing .dev/e2e/Dockerfile) +make docker-build-e2e + +# Run against the default matrix cell (PHP 8.3, WP 6.9.4) +make e2e + +# Run against a specific PHP × WP version +make e2e PHP=8.0 WP=6.8 +``` + +Supported WP versions: `6.8`, `6.9`, `7` (must be `>= 6.8` — the plugin's declared minimum). + +On failure, Playwright's HTML report is written to `tests/e2e/playwright-report/` inside the container. Pass `-e PWDEBUG=1` in the `docker run` command to enable step-by-step debugging. + +### What the tests cover + +| Spec | What it verifies | +|------|-----------------| +| `plugin-activation.spec.ts` | Plugin is present and `active` via `GET /wp/v2/plugins` | +| `mcp-tools.spec.ts` | MCP `tools/list` returns `gd-mcp-get-site-info`; `tools/call` returns site data | + +### How it works + +1. `entrypoint.sh` runs inside the container: + - Builds a distributable zip via `.dev/release/build-zip.sh` (Composer, DSG sync, wp-scripts build, plugin-zip) + - Extracts `builds/airo-wp-test.zip` to a temp directory + - Copies `tests/e2e/setup/e2e-setup.php` into the @wp-playground/cli server's MU-plugins directory (loaded on WP boot) + - Starts `@wp-playground/cli` serving the extracted plugin on port 8080 + - Runs `playwright test` with static test credentials +2. All REST calls use `index.php?rest_route=` instead of `/wp-json/` because @wp-playground/cli does not rewrite URLs at the Node level. + +### CI + +The workflow at `.github/workflows/e2e.yml` is **manual only** (`workflow_dispatch`). It accepts optional `php_version` and `wp_version` inputs (defaults to `all`) and runs the full PHP × WP matrix with `fail-fast: false`. Trigger it from the GitHub Actions tab when you need to verify backward compatibility. + +## WP Playground + +A local WordPress instance running the plugin with live source binding — useful for quick fixes, prototyping, and manual testing. + +### Running + +```bash +# Start (first run builds the image and installs deps) +make playground + +# Specific PHP and WP versions +make playground PHP=8.2 WP=6.8 + +# Stop +make playground-stop + +# Reset all state (fresh WordPress on next start) +make playground-reset +``` + +### How it works + +- Uses [`@wp-playground/cli`](https://github.com/WordPress/playground-tools/tree/trunk/packages/playground-cli) (WordPress Playground for Node.js) inside Docker +- Plugin source is bind-mounted — edits reflect immediately without restart +- WordPress state (SQLite database, uploads, settings) persists in the `airo-wp-playground-data` Docker volume across container restarts +- Browser auto-opens at `http://localhost:9400`; admin at `http://localhost:9400/wp-admin/` +- The Docker image is built from the `base` stage of `.dev/e2e/Dockerfile` (PHP + Node 24 + Composer, shared with the e2e image) + +### Known limitations + +- @wp-playground/cli uses PHP-WASM (WebAssembly), not native PHP — step-debugging (Xdebug) is not available +- WordPress loopback requests (Site Health REST API check) time out — this is a PHP-WASM limitation and does not affect normal plugin development +- Plugin source changes are live, but `vendor/` changes require stopping and restarting (`make playground-stop && make playground`) + +## Architecture + +| Path | Role | +|------|------| +| `airo-wp.php` | Plugin header, constants, autoload, container, `Plugin::instance()` | +| `includes/` | PSR-4 code — `GoDaddy\WordPress\Plugins\AiroWp\` | +| `includes/Packages.php` | Domain package registry | +| `includes/Container.php` | DI facade | +| `functions/` | Global helpers (`functions/index.php` → per-domain files) | +| `dependencies/` | Strauss-prefixed runtime vendors (gitignored; generated at build time) | +| `tests/` | PHPUnit unit tests | + +**Autoloading:** Composer's `vendor/autoload.php` loads plugin code (`includes/`, `functions/`) and Strauss-prefixed runtime packages under `dependencies/`. Run `composer install` (or `composer install --no-dev` for releases) before activating the plugin. + +## Project structure + +``` +airo-wp.php # Plugin header + bootstrap (keep thin) +readme.txt # WordPress.org-style readme (screens, changelog) +includes/ # All PHP classes (PSR-4) + Container.php + Packages.php + Plugin.php + Internal/ # Infrastructure (container, etc.) + {YourDomain}/ # Your feature package + Package.php +functions/ + index.php # Composer files autoload entry + container.php # AiroWP() + {your-domain}.php # Optional global helpers +dependencies/ # Strauss-prefixed vendors (gitignored; generated by composer build) +tests/ # PHPUnit + Brain Monkey +``` + +Namespace root: `GoDaddy\WordPress\Plugins\AiroWp\` + +## Adding a feature (domain package) + +1. **Create the package class** + + `includes/MyFeature/Package.php`: + + ```php + MyFeature\Package::class, + ``` + +3. **Prefer DI over `new`** — resolve services via `$container->get( SomeService::class )` with constructor or `init()` type hints. + +4. **Optional helpers** — add `functions/my-feature.php` and `require_once` it from `functions/index.php`. + +5. **Tests** — add `tests/Unit/MyFeature/` with Brain Monkey for WordPress APIs. + +6. **Run** `composer test`, `composer lint`, and `composer build` if you changed Composer dependencies (`dependencies/` is regenerated automatically and should not be committed). + +## Coding standards + +- **PHPCS** with WordPress-Core, WordPress-Extra, WordPress-Docs, **WordPress-VIP-Go**, and PHPCompatibilityWP (`testVersion` 7.4–8.3). +- Run `make lint` (Docker, matches CI) before pushing; `composer lint` works too if Docker is unavailable. `composer format` (or `phpcbf`) fixes many issues automatically. +- Use `declare(strict_types=1);` in new PHP files. +- Add `defined( 'ABSPATH' ) || exit;` to every PHP file for direct file access protection. Place it **after** the `namespace` declaration in namespaced files, or after `declare(strict_types=1)` in non-namespaced files (PHP requires `namespace` to directly follow `declare`). +- Keep `airo-wp.php` free of business logic. +- Write PHP **7.4-compatible** code in `includes/` (no enums/readonly in shared code unless the team agrees to raise the minimum). +- **Brain Monkey** for unit tests; use `Proxies\LegacyProxy` only when a WordPress function is impractical to mock. + +## Dependency and autoload rules + +- **Runtime** packages go in `composer.json` `require` → Strauss prefixes them into `dependencies/`. +- **Dev-only** tools stay in `require-dev` → `vendor/` only, not shipped. +- Do not commit `vendor/`, `dependencies/`, or `bin/strauss.phar`. +- `dependencies/` is gitignored and generated automatically by `composer build` (or `make test` on first run). + +## Pull requests + +1. Branch from `main` (or your team's default branch). +2. Keep changes focused; one domain or one infrastructure concern per PR when possible. +3. Ensure CI passes (PHPUnit matrix 7.4–8.3, PHPCS on 8.3). +4. Describe **what** changed and **why** in the PR body. +5. No secrets, `.env` files, or local paths in commits. + +## Local docs + +Brainstorming specs and implementation plans may live under `docs/superpowers/` on your machine. That directory is **gitignored** and is not part of the plugin distribution. + +## Questions + +For team-specific process (code review owners, release cadence, Jira tickets), use your GoDaddy / Site Designer team channels. For plugin architecture questions, refer to `README.md` and the inline structure in `includes/Packages.php` and `includes/Container.php`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e477df --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Airo WP AI Builder + +[![PHPUnit](https://github.com/gdcorp-wordpress/airo-wp/actions/workflows/phpunit.yml/badge.svg)](https://github.com/gdcorp-wordpress/airo-wp/actions/workflows/phpunit.yml) +[![PHPCS](https://github.com/gdcorp-wordpress/airo-wp/actions/workflows/phpcs.yml/badge.svg)](https://github.com/gdcorp-wordpress/airo-wp/actions/workflows/phpcs.yml) + +**Airo WP AI Builder** is a WordPress plugin that exposes your site to AI assistants via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). It also bundles a curated set of Gutenberg block patterns built on the Twenty Twenty-Five (tt5) theme, giving any connected AI a ready-made vocabulary for assembling pages. + +## What's included + +- **MCP Server** — Registers an MCP-compatible endpoint. Tools cover posts, pages, media, templates, navigation menus, and global styles. +- **Block Patterns** — Curated Gutenberg patterns for the Twenty Twenty-Five theme, sourced from [DesignSetGo](https://wordpress.org/plugins/designsetgo/) (deferred automatically when DesignSetGo is active). +- **AI-agnostic** — Connect Claude, GPT, Gemini, or any MCP-compatible client. No specific AI is bundled or required. + +## Roadmap (v1) + +- **Site generation** — build a complete WordPress site from a single prompt +- **Conversational setup** — add pages, swap images, and update layouts through plain-English chat +- **WooCommerce storefront** — auto-configure a storefront with AI-generated products from a single prompt + +## Plugin information + +| | | +|---|---| +| **Plugin name** | Airo WP AI Builder | +| **Text domain** | `airo-wp` | +| **Version** | 0.2.4 | +| **Requires WordPress** | 6.8+ | +| **Requires PHP** | 7.4+ | +| **License** | [GPLv2 or later](https://www.gnu.org/licenses/gpl-2.0.html) | +| **Author** | [GoDaddy](https://www.godaddy.com) | +| **Repository** | [gdcorp-wordpress/airo-wp](https://github.com/gdcorp-wordpress/airo-wp) | + +## Installation + +1. Copy or deploy this repository to `wp-content/plugins/airo-wp/`. +2. Activate **Airo WP AI Builder** under **Plugins** in wp-admin. + +## Contributing + +See **[CONTRIBUTORS.md](CONTRIBUTORS.md)** for environment setup, architecture, coding standards, and pull request expectations. + +AI assistants should read **[AGENTS.md](AGENTS.md)** (`CLAUDE.md` symlinks to it) for repository-specific commands and architecture. + +## License + +GPLv2 or later. See [LICENSE](https://www.gnu.org/licenses/gpl-2.0.html). diff --git a/airo-wp.php b/airo-wp.php new file mode 100644 index 0000000..b6013a5 --- /dev/null +++ b/airo-wp.php @@ -0,0 +1,46 @@ +=7.4", + "ext-json": "*", + "psr/container": "^2.0", + "wordpress/abilities-api": "^0.4.0", + "wordpress/mcp-adapter": "^0.4.1" + }, + "require-dev": { + "automattic/vipwpcs": "^3.0", + "brain/monkey": "^2.6", + "dealerdirect/phpcodesniffer-composer-installer": "^1.2.0", + "mockery/mockery": "^1.5", + "php-stubs/wordpress-stubs": "^6.8", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpunit/phpunit": "^9.6", + "wp-coding-standards/wpcs": "^3.0" + }, + "autoload": { + "psr-4": { + "GoDaddy\\WordPress\\Plugins\\AiroWp\\": "includes/", + "GoDaddy\\WordPress\\Plugins\\AiroWp\\Dependencies\\Psr\\Container\\": "dependencies/psr/container/src", + "GoDaddy\\WordPress\\Plugins\\AiroWp\\Dependencies\\": "dependencies/" + }, + "files": [ + "functions/index.php" + ] + }, + "autoload-dev": { + "psr-4": { + "GoDaddy\\WordPress\\Plugins\\AiroWp\\Tests\\": "tests/" + } + }, + "config": { + "platform": { + "php": "7.4" + }, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true, + "platform-check": false + }, + "extra": { + "strauss": { + "target_directory": "dependencies", + "namespace_prefix": "GoDaddy\\WordPress\\Plugins\\AiroWp\\Dependencies\\", + "classmap_prefix": "AIRO_WP_", + "update_call_sites": [ + "includes", + "functions", + "airo-wp.php" + ], + "delete_vendor_packages": true + } + }, + "scripts": { + "lint": "phpcs --standard=phpcs.xml", + "format": "phpcbf --standard=phpcs.xml --report-summary --report-source", + "test": "php -d auto_prepend_file=tests/constants.php vendor/bin/phpunit --colors=always", + "prefix-namespaces": [ + "sh -c 'test -f ./bin/strauss.phar || (mkdir -p bin && curl -fsSL -o bin/strauss.phar https://github.com/BrianHenryIE/strauss/releases/latest/download/strauss.phar)'", + "@php bin/strauss.phar", + "@composer dump-autoload" + ], + "post-install-cmd": [ + "@prefix-namespaces" + ], + "post-update-cmd": [ + "@prefix-namespaces" + ], + "post-autoload-dump": [ + "@php -r \"if (file_exists('bin/strauss.phar')) { passthru('php bin/strauss.phar include-autoloader'); }\"" + ], + "build": [ + "@prefix-namespaces" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..99275d8 --- /dev/null +++ b/composer.lock @@ -0,0 +1,3067 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "80d70621e97d640336dfa6e324027f73", + "packages": [ + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "wordpress/abilities-api", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/abilities-api.git", + "reference": "0759075aed37c4247adbf273bdebec096d52e825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/abilities-api/zipball/0759075aed37c4247adbf273bdebec096d52e825", + "reference": "0759075aed37c4247adbf273bdebec096d52e825", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8" + }, + "require-dev": { + "automattic/vipwpcs": "^3.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "phpcompatibility/php-compatibility": "10.x-dev as 9.99.99", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^2.1.22", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.3", + "phpunit/phpunit": "^8.5|^9.6", + "slevomat/coding-standard": "^8.0", + "squizlabs/php_codesniffer": "^3.9", + "szepeviktor/phpstan-wordpress": "^2.0.2", + "wp-coding-standards/wpcs": "^3.1", + "wp-phpunit/wp-phpunit": "^6.5", + "wpackagist-plugin/plugin-check": "^1.6", + "yoast/phpunit-polyfills": "^4.0" + }, + "type": "library", + "extra": { + "installer-paths": { + "vendor/{$vendor}/{$name}/": [ + "wpackagist-plugin/plugin-check" + ] + } + }, + "autoload": { + "files": [ + "includes/bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "WordPress AI Team", + "homepage": "https://make.wordpress.org/ai/" + } + ], + "description": "AI Abilities for WordPress.", + "homepage": "https://github.com/WordPress/abilities-api", + "keywords": [ + "abilities", + "ai", + "api", + "llm", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/abilities-api/issues", + "source": "https://github.com/WordPress/abilities-api" + }, + "abandoned": true, + "time": "2025-10-29T05:35:31+00:00" + }, + { + "name": "wordpress/mcp-adapter", + "version": "v0.4.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/mcp-adapter.git", + "reference": "1a0f9ab868e34b4375be42e873f60765e6632505" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/mcp-adapter/zipball/1a0f9ab868e34b4375be42e873f60765e6632505", + "reference": "1a0f9ab868e34b4375be42e873f60765e6632505", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "automattic/vipwpcs": "^3.0", + "php-stubs/wp-cli-stubs": "^2.12", + "phpcompatibility/php-compatibility": "10.x-dev as 9.99.99", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpstan/extension-installer": "^1.3", + "phpstan/php-8-stubs": "^0.4.24", + "phpstan/phpstan": "^2.1.22", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.0", + "szepeviktor/phpstan-wordpress": "^2.0", + "wp-phpunit/wp-phpunit": "^6.5", + "wpackagist-plugin/plugin-check": "^1.6", + "yoast/phpunit-polyfills": "^4.0" + }, + "type": "library", + "extra": { + "installer-paths": { + "vendor/{$vendor}/{$name}/": [ + "wpackagist-plugin/plugin-check" + ] + } + }, + "autoload": { + "psr-4": { + "WP\\MCP\\": "includes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "WordPress AI Team", + "homepage": "https://make.wordpress.org/ai/" + } + ], + "description": "Adapter for Abilities API, letting WordPress abilities to be used as MCP tools, resources or prompts", + "homepage": "https://github.com/wordpress/mcp-adapter", + "keywords": [ + "abilities-api", + "adapter", + "ai", + "api", + "integration", + "mcp", + "model-context-protocol", + "wordpress" + ], + "support": { + "issues": "https://github.com/wordpress/mcp-adapter/issues", + "source": "https://github.com/wordpress/mcp-adapter" + }, + "time": "2025-12-09T06:56:51+00:00" + } + ], + "packages-dev": [ + { + "name": "antecedent/patchwork", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/antecedent/patchwork.git", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "reference": "8b6b235f405af175259c8f56aea5fc23ab9f03ce", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignas Rudaitis", + "email": "ignas.rudaitis@gmail.com" + } + ], + "description": "Method redefinition (monkey-patching) functionality for PHP.", + "homepage": "https://antecedent.github.io/patchwork/", + "keywords": [ + "aop", + "aspect", + "interception", + "monkeypatching", + "redefinition", + "runkit", + "testing" + ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.2.3" + }, + "time": "2025-09-17T09:00:56+00:00" + }, + { + "name": "automattic/vipwpcs", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/Automattic/VIP-Coding-Standards.git", + "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/2b1d206d81b74ed999023cffd924f862ff2753c8", + "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsextra": "^1.2.1", + "phpcsstandards/phpcsutils": "^1.0.11", + "sirbrillig/phpcs-variable-analysis": "^2.11.18", + "squizlabs/php_codesniffer": "^3.9.2", + "wp-coding-standards/wpcs": "^3.1.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/Automattic/VIP-Coding-Standards/issues", + "source": "https://github.com/Automattic/VIP-Coding-Standards", + "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki" + }, + "time": "2024-05-10T20:31:09+00:00" + }, + { + "name": "brain/monkey", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/Brain-WP/BrainMonkey.git", + "reference": "ea3aeb3d559ba3c0930b3f4d210b665a4c044d83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/ea3aeb3d559ba3c0930b3f4d210b665a4c044d83", + "reference": "ea3aeb3d559ba3c0930b3f4d210b665a4c044d83", + "shasum": "" + }, + "require": { + "antecedent/patchwork": "^2.1.17", + "mockery/mockery": "~1.3.6 || ~1.4.4 || ~1.5.1 || ^1.6.10", + "php": ">=5.6.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "phpcompatibility/php-compatibility": "^9.3.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.49 || ^9.6.30" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev", + "dev-version/1": "1.x-dev" + } + }, + "autoload": { + "files": [ + "inc/api.php" + ], + "psr-4": { + "Brain\\Monkey\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Giuseppe Mazzapica", + "email": "giuseppe.mazzapica@gmail.com", + "homepage": "https://gmazzap.me", + "role": "Developer" + } + ], + "description": "Mocking utility for PHP functions and WordPress plugin API", + "keywords": [ + "Monkey Patching", + "interception", + "mock", + "mock functions", + "mockery", + "patchwork", + "redefinition", + "runkit", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/Brain-WP/BrainMonkey/issues", + "source": "https://github.com/Brain-WP/BrainMonkey" + }, + "time": "2026-02-05T09:22:14+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.1", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" + }, + "time": "2026-02-03T19:29:21+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-09-19T17:43:28+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.8", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-10-18T00:05:59+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.34", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:45:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:22:56+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.13.0", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "a15e970b8a0bf64cfa5e86d941f5e6b08855f369" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/a15e970b8a0bf64cfa5e86d941f5e6b08855f369", + "reference": "a15e970b8a0bf64cfa5e86d941f5e6b08855f369", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.7 || ^4.0.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpstan/phpstan": "^1.7 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], + "support": { + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + }, + "time": "2025-09-30T22:22:48+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4", + "ext-json": "*" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "7.4" + }, + "plugin-api-version": "2.9.0" +} diff --git a/data/block-animation-attributes.php b/data/block-animation-attributes.php new file mode 100644 index 0000000..124f325 --- /dev/null +++ b/data/block-animation-attributes.php @@ -0,0 +1,205 @@ + '', + 'attrs' => '', + ); + } + + // Add animation classes. + $animation_classes[] = 'has-dsgo-animation'; + + // Add entrance animation class. + $entrance_animation = isset( $attributes['dsgoEntranceAnimation'] ) ? $attributes['dsgoEntranceAnimation'] : ''; + if ( $entrance_animation ) { + $animation_classes[] = 'dsgo-animation-' . esc_attr( $entrance_animation ); + } + + // Add exit animation class. + $exit_animation = isset( $attributes['dsgoExitAnimation'] ) ? $attributes['dsgoExitAnimation'] : ''; + if ( $exit_animation ) { + $animation_classes[] = 'dsgo-animation-exit-' . esc_attr( $exit_animation ); + } + + // Always include the enabled flag and animation type(s) — required by frontend JS. + $animation_attrs['data-dsgo-animation-enabled'] = 'true'; + + if ( $entrance_animation ) { + $animation_attrs['data-dsgo-entrance-animation'] = esc_attr( $entrance_animation ); + } + + if ( $exit_animation ) { + $animation_attrs['data-dsgo-exit-animation'] = esc_attr( $exit_animation ); + } + + // Only output settings that differ from defaults to keep markup lean. + // Defaults: trigger=scroll, duration=600, delay=0, easing=ease-out, offset=100, once=true. + $trigger = isset( $attributes['dsgoAnimationTrigger'] ) ? $attributes['dsgoAnimationTrigger'] : 'scroll'; + if ( 'scroll' !== $trigger ) { + $animation_attrs['data-dsgo-animation-trigger'] = esc_attr( $trigger ); + } + + $duration = isset( $attributes['dsgoAnimationDuration'] ) ? (int) $attributes['dsgoAnimationDuration'] : 600; + if ( 600 !== $duration ) { + $animation_attrs['data-dsgo-animation-duration'] = esc_attr( (string) $duration ); + } + + $delay = isset( $attributes['dsgoAnimationDelay'] ) ? (int) $attributes['dsgoAnimationDelay'] : 0; + if ( 0 !== $delay ) { + $animation_attrs['data-dsgo-animation-delay'] = esc_attr( (string) $delay ); + } + + $easing = isset( $attributes['dsgoAnimationEasing'] ) ? $attributes['dsgoAnimationEasing'] : 'ease-out'; + if ( 'ease-out' !== $easing ) { + $animation_attrs['data-dsgo-animation-easing'] = esc_attr( $easing ); + } + + $offset = isset( $attributes['dsgoAnimationOffset'] ) ? (int) $attributes['dsgoAnimationOffset'] : 100; + if ( 100 !== $offset ) { + $animation_attrs['data-dsgo-animation-offset'] = esc_attr( (string) $offset ); + } + + $once = isset( $attributes['dsgoAnimationOnce'] ) ? (bool) $attributes['dsgoAnimationOnce'] : true; + if ( ! $once ) { + $animation_attrs['data-dsgo-animation-once'] = 'false'; + } + + // Convert classes array to string. + $classes_string = implode( ' ', $animation_classes ); + + // Convert data attributes array to string. + $attrs_string = ''; + foreach ( $animation_attrs as $key => $value ) { + $attrs_string .= ' ' . $key . '="' . $value . '"'; + } + + return array( + 'classes' => $classes_string, + 'attrs' => $attrs_string, + ); +} + +/** + * Get clickable link data attributes from block attributes + * + * Extracts link-related attributes for the clickable-group extension. + * + * @param array $attributes Block attributes array. + * @return array Array of data attributes for links. + */ +function designsetgo_get_clickable_attributes( $attributes ) { + $link_attrs = array(); + $link_classes = array(); + + $link_url = isset( $attributes['dsgLinkUrl'] ) ? $attributes['dsgLinkUrl'] : ''; + + if ( empty( $link_url ) ) { + return array( + 'classes' => '', + 'attrs' => '', + ); + } + + // Add clickable class. + $link_classes[] = 'dsgo-clickable'; + + // Add link data attributes. + $link_attrs['data-link-url'] = esc_attr( $link_url ); + + $link_target = isset( $attributes['dsgLinkTarget'] ) ? $attributes['dsgLinkTarget'] : false; + if ( $link_target ) { + $link_attrs['data-link-target'] = '_blank'; + } + + $link_rel = isset( $attributes['dsgLinkRel'] ) ? $attributes['dsgLinkRel'] : ''; + if ( $link_rel ) { + $link_attrs['data-link-rel'] = esc_attr( $link_rel ); + } + + // Convert classes array to string. + $classes_string = implode( ' ', $link_classes ); + + // Convert data attributes array to string. + $attrs_string = ''; + foreach ( $link_attrs as $key => $value ) { + $attrs_string .= ' ' . $key . '="' . $value . '"'; + } + + return array( + 'classes' => $classes_string, + 'attrs' => $attrs_string, + ); +} + +/** + * Add animation and extension attributes to wrapper attributes string + * + * Takes an existing wrapper attributes string (from get_block_wrapper_attributes) + * and injects animation classes/data attributes plus other extension attributes. + * + * @param string $wrapper_attributes Existing wrapper attributes string. + * @param array $attributes Block attributes array. + * @return string Modified wrapper attributes string. + */ +function designsetgo_add_animation_to_wrapper( $wrapper_attributes, $attributes ) { + // Get animation data. + $animation_data = designsetgo_get_animation_attributes( $attributes ); + + // Get clickable link data. + $clickable_data = designsetgo_get_clickable_attributes( $attributes ); + + // Combine all classes. + $all_classes = trim( $animation_data['classes'] . ' ' . $clickable_data['classes'] ); + + // Combine all data attributes. + $all_attrs = $animation_data['attrs'] . $clickable_data['attrs']; + + // Add classes to existing class attribute if we have any. + if ( ! empty( $all_classes ) ) { + if ( strpos( $wrapper_attributes, 'class="' ) !== false ) { + $wrapper_attributes = preg_replace( + '/class="([^"]*)"/', + 'class="$1 ' . $all_classes . '"', + $wrapper_attributes + ); + } else { + // Add new class attribute. + $wrapper_attributes .= ' class="' . $all_classes . '"'; + } + } + + // Append all data attributes. + $wrapper_attributes .= $all_attrs; + + return $wrapper_attributes; +} diff --git a/data/icon-aliases.json b/data/icon-aliases.json new file mode 100644 index 0000000..cc6cbc3 --- /dev/null +++ b/data/icon-aliases.json @@ -0,0 +1,298 @@ +{ + "email": "envelope", + "mail": "envelope", + "inbox": "envelope", + "message": "envelope", + "zap": "lightning", + "bolt": "lightning", + "electricity": "lightning", + "thunder": "lightning", + "check-circle": "circle-check", + "checkmark": "circle-check", + "checkmark-circle": "circle-check", + "verified": "verified-check", + "badge": "verified-check", + "favorite": "star", + "rating": "star", + "love": "heart", + "like": "heart", + "person": "user", + "profile": "user", + "account": "user", + "people": "users", + "team": "users", + "group": "users", + "launch": "rocket", + "startup": "rocket", + "idea": "lightbulb", + "tip": "lightbulb", + "bulb": "lightbulb", + "award": "trophy", + "winner": "trophy", + "prize": "trophy", + "call": "phone", + "telephone": "phone", + "mobile": "phone", + "map": "location", + "map-pin": "location", + "address": "location", + "place": "location", + "time": "clock", + "schedule": "clock", + "timer": "clock", + "thumbs-up-alt": "thumbs-up", + "approve": "thumbs-up", + "present": "gift", + "reward": "gift", + "security": "shield", + "protect": "shield", + "shield-alt": "shield", + "security-check": "shield-check", + "protected": "shield-check", + "fb": "facebook", + "x": "twitter", + "tweet": "twitter", + "ig": "instagram", + "li": "linkedin", + "yt": "youtube", + "gh": "github", + "wa": "whatsapp", + "cart": "shopping-cart", + "basket": "shopping-cart", + "ecommerce": "shopping-cart", + "payment": "credit-card", + "visa": "credit-card", + "mastercard": "credit-card", + "label": "tag", + "price": "tag", + "money": "dollar", + "currency": "dollar", + "usd": "dollar", + "money-bag": "wallet", + "purse": "wallet", + "shop": "store", + "storefront": "store", + "bubble": "comment", + "feedback": "comment", + "messaging": "chat", + "conversation": "chat", + "notification": "bell", + "alert": "bell", + "alarm": "bell", + "help": "info", + "information": "info", + "caution": "warning", + "danger": "warning", + "blockquote": "quote", + "quotation": "quote", + "cite": "quote", + "add": "plus", + "create": "plus", + "new": "plus", + "subtract": "minus", + "remove": "minus", + "close": "times", + "cancel": "times", + "cross": "times", + "x-mark": "times", + "pencil": "edit", + "modify": "edit", + "write": "edit", + "delete": "trash", + "bin": "trash", + "rubbish": "trash", + "magnifying-glass": "search", + "find": "search", + "lookup": "search", + "gear": "settings", + "cog": "settings", + "preferences": "settings", + "config": "settings", + "hamburger": "menu", + "nav": "menu", + "navigation": "menu", + "reload": "refresh", + "sync": "refresh", + "rotate": "refresh-cw", + "film": "video", + "movie": "video", + "photo": "image", + "picture": "image", + "gallery": "image", + "img": "image", + "photograph": "camera", + "snapshot": "camera", + "right-arrow": "arrow-right", + "left-arrow": "arrow-left", + "up-arrow": "arrow-up", + "down-arrow": "arrow-down", + "next": "chevron-right", + "forward": "chevron-right", + "previous": "chevron-left", + "back": "chevron-left", + "house": "home", + "dashboard": "home", + "save": "bookmark", + "saved": "bookmark", + "url": "link", + "href": "link", + "chain": "link", + "hyperlink": "link", + "directory": "folder", + "document": "file", + "doc": "file", + "page": "file", + "programming": "code", + "developer": "code", + "dev": "code", + "duplicate": "copy", + "clone": "copy", + "work": "briefcase", + "job": "briefcase", + "portfolio": "briefcase", + "office": "building", + "company": "building", + "corporate": "building", + "analytics": "chart", + "graph": "chart", + "statistics": "chart", + "stats": "chart", + "date": "calendar", + "event": "calendar", + "appointment": "calendar", + "diploma": "certificate", + "achievement": "certificate", + "computer": "monitor", + "desktop": "monitor", + "screen": "monitor", + "display": "monitor", + "iphone": "smartphone", + "android": "smartphone", + "ipad": "tablet", + "internet": "wifi", + "wireless": "wifi", + "network": "wifi", + "db": "database", + "server": "database", + "storage": "database", + "cloud-computing": "cloud", + "saas": "cloud", + "reading": "book", + "education": "graduation-cap", + "university": "graduation-cap", + "school": "graduation-cap", + "graduate": "graduation-cap", + "degree": "graduation-cap", + "gym": "fitness", + "exercise": "fitness", + "workout": "dumbbell", + "weight": "dumbbell", + "health": "medkit", + "medical": "medkit", + "first-aid": "medkit", + "hospital": "medkit", + "nature": "leaf", + "eco": "leaf", + "plant": "leaf", + "organic": "leaf", + "flame": "fire", + "hot": "fire", + "trending": "fire", + "package": "box", + "shipping": "truck", + "delivery": "truck", + "logistics": "truck", + "audio": "speaker", + "sound": "speaker", + "volume": "speaker", + "mic": "microphone", + "record": "microphone", + "podcast": "microphone", + "headset": "headphones", + "earphones": "headphones", + "slides": "presentation", + "slideshow": "presentation", + "power-point": "presentation", + "goal": "target", + "aim": "target", + "bullseye": "target", + "deal": "handshake", + "partner": "handshake", + "agreement": "handshake", + "world": "globe", + "earth": "globe", + "international": "globe", + "thumbtack": "pin", + "pushpin": "pin", + "travel": "plane", + "flight": "plane", + "airplane": "plane", + "vehicle": "car", + "automobile": "car", + "drive": "car", + "accommodation": "hotel", + "lodging": "hotel", + "bed": "hotel", + "direction": "compass", + "explore": "compass", + "luggage": "suitcase", + "baggage": "suitcase", + "cafe": "coffee", + "tea": "coffee", + "food": "restaurant", + "dining": "restaurant", + "eat": "restaurant", + "song": "music", + "melody": "music", + "tunes": "music", + "tokens": "coin", + "coins": "coin", + "finance": "bank", + "banking": "bank", + "growth": "trending-up", + "increase": "trending-up", + "arrow-trending": "trending-up", + "power": "battery", + "energy": "battery", + "print": "printer", + "click": "cursor-click", + "cursor": "cursor-click", + "pointer": "cursor-click", + "visible": "eye", + "view": "eye", + "show": "eye", + "hidden": "eye-off", + "invisible": "eye-off", + "hide": "eye-off", + "switch": "toggle", + "toggle-off": "toggle", + "switch-on": "toggle-on", + "scan": "barcode", + "qr": "barcode", + "invoice": "receipt", + "bill": "receipt", + "percent": "percentage", + "discount": "percentage", + "sale": "percentage", + "input": "text-field", + "text-input": "text-field", + "number-input": "number-field", + "form-field": "form", + "signup": "form", + "contact": "at-sign", + "at-symbol": "at-sign", + "submit": "send", + "paper-plane": "send", + "tier": "layers", + "depth": "layers", + "layout": "grid", + "grid-view": "grid", + "list-view": "list", + "rows": "list", + "pad": "keyboard", + "typing": "keyboard", + "secure": "lock", + "password": "lock", + "lock-open": "unlock", + "unsecure": "unlock" +} diff --git a/data/icon-svg-library.php b/data/icon-svg-library.php new file mode 100644 index 0000000..6e8ce5e --- /dev/null +++ b/data/icon-svg-library.php @@ -0,0 +1,344 @@ +%s', + esc_attr( (string) $stroke ), + $svg + ); + } + + return $svg; +} + +/** + * Get all available icons + * + * @return array Associative array of icon_name => svg_markup. + */ +function designsetgo_get_all_icons() { + static $icons = null; + + // Cache icons in static variable for performance. + if ( null !== $icons ) { + return $icons; + } + + $icons = array( + 'accordion' => '', + 'arrow-down' => '', + 'arrow-left' => '', + 'arrow-right' => '', + 'arrow-up' => '', + 'at-sign' => '', + 'bank' => '', + 'barcode' => '', + 'battery' => '', + 'bell' => '', + 'blocks' => '', + 'book' => '', + 'bookmark' => '', + 'box' => '', + 'briefcase' => '', + 'building' => '', + 'calendar' => '', + 'camera' => '', + 'car' => '', + 'certificate' => '', + 'chart' => '', + 'chat' => '', + 'check' => '', + 'checkbox' => '', + 'checkbox-checked' => '', + 'chevron-left' => '', + 'chevron-right' => '', + 'circle-check' => '', + 'clipboard' => '', + 'clock' => '', + 'cloud' => '', + 'code' => '', + 'coffee' => '', + 'coin' => '', + 'comment' => '', + 'compass' => '', + 'container' => '', + 'copy' => '', + 'counter' => '', + 'countdown' => '', + 'credit-card' => '', + 'cursor-click' => '', + 'database' => '', + 'discord' => '', + 'dollar' => '', + 'download' => '', + 'dropdown' => '', + 'dumbbell' => '', + 'edit' => '', + 'envelope' => '', + 'eye' => '', + 'eye-off' => '', + 'facebook' => '', + 'file' => '', + 'filter' => '', + 'fire' => '', + 'fitness' => '', + 'flex' => '', + 'flip-card' => '', + 'folder' => '', + 'form' => '', + 'gift' => '', + 'github' => '', + 'globe' => '', + 'graduation-cap' => '', + 'grid' => '', + 'handshake' => '', + 'headphones' => '', + 'heart' => '', + 'home' => '', + 'hotel' => '', + 'icon-button' => '', + 'icon-list' => '', + 'image' => '', + 'image-accordion' => '', + 'info' => '', + 'instagram' => '', + 'keyboard' => '', + 'layers' => '', + 'leaf' => '', + 'lightbulb' => '', + 'lightning' => '', + 'link' => '', + 'linkedin' => '', + 'list' => '', + 'location' => '', + 'lock' => '', + 'medkit' => '', + 'marquee' => '', + 'menu' => '', + 'microphone' => '', + 'minus' => '', + 'monitor' => '', + 'moon' => '', + 'mouse' => '', + 'music' => '', + 'number-field' => '', + 'pause' => '', + 'percentage' => '', + 'phone' => '', + 'pill' => '', + 'pin' => '', + 'pinterest' => '', + 'plane' => '', + 'play' => '', + 'plus' => '', + 'presentation' => '', + 'printer' => '', + 'progress-bar' => '', + 'radio' => '', + 'radio-checked' => '', + 'quote' => '', + 'receipt' => '', + 'reddit' => '', + 'refresh' => '', + 'refresh-cw' => '', + 'restaurant' => '', + 'reveal' => '', + 'rocket' => '', + 'search' => '', + 'send' => '', + 'settings' => '', + 'share' => '', + 'shield' => '', + 'shield-check' => '', + 'shopping-cart' => '', + 'slack' => '', + 'slider' => '', + 'smartphone' => '', + 'speaker' => '', + 'star' => '', + 'stack' => '', + 'store' => '', + 'suitcase' => '', + 'sun' => '', + 'tablet' => '', + 'tabs' => '', + 'tag' => '', + 'target' => '', + 'text-field' => '', + 'thumbs-up' => '', + 'tiktok' => '', + 'times' => '', + 'toggle' => '', + 'toggle-on' => '', + 'trash' => '', + 'trending-up' => '', + 'trophy' => '', + 'truck' => '', + 'x' => '', + // Backward compatibility: 'twitter' was renamed to 'x'. Existing saved blocks may reference data-icon-name="twitter". + 'twitter' => '', + 'unlock' => '', + 'upload' => '', + 'user' => '', + 'users' => '', + 'verified-check' => '', + 'video' => '', + 'wallet' => '', + 'warning' => '', + 'whatsapp' => '', + 'wifi' => '', + 'youtube' => '', + ); + + return $icons; +} + +/** + * Get icon alias map. + * + * Maps alternate icon names to their canonical names in the icon library. + * Single source of truth: includes/data/icon-aliases.json (shared with JS). + * + * @return array Associative array of alias => canonical_name. + */ +function designsetgo_get_icon_aliases() { + static $aliases = null; + + if ( null !== $aliases ) { + return $aliases; + } + + $file = __DIR__ . '/data/icon-aliases.json'; + + if ( ! file_exists( $file ) ) { + $aliases = array(); + return $aliases; + } + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown -- Reading local plugin file. + $aliases = json_decode( file_get_contents( $file ), true ) ?? array(); + + return $aliases; +} + +/** + * Sanitize icon slug to prevent XSS + * + * Allow only: lowercase letters, numbers, hyphens. + * + * @param string $icon Icon slug to sanitize. + * @return string Sanitized icon slug. + */ +function designsetgo_sanitize_icon_slug( $icon ) { + if ( empty( $icon ) || ! is_string( $icon ) ) { + return ''; + } + // Only allow safe characters for icon names. + return preg_replace( '/[^a-z0-9\-]/', '', strtolower( $icon ) ); +} + +/** + * Render accordion icon SVG + * + * Generates icon SVG for accordion items. + * + * @param string $icon_style Icon style (chevron, plus-minus, caret, none). + * @param bool $is_open Whether accordion item is open. + * @return string Icon HTML markup. + */ +function designsetgo_accordion_render_icon( $icon_style, $is_open ) { + if ( 'none' === $icon_style ) { + return ''; + } + + $icon_svg = ''; + + switch ( $icon_style ) { + case 'plus-minus': + if ( $is_open ) { + $icon_svg = ''; + } else { + $icon_svg = ''; + } + break; + case 'caret': + $icon_svg = ''; + break; + case 'chevron': + default: + $icon_svg = ''; + } + + return ''; +} diff --git a/data/svg-pattern-data.php b/data/svg-pattern-data.php new file mode 100644 index 0000000..1fe8ec0 --- /dev/null +++ b/data/svg-pattern-data.php @@ -0,0 +1,322 @@ + Pattern definitions keyed by pattern ID. + */ +function designsetgo_get_svg_pattern_data() { + return array( + // ── MINIMAL ────────────────────────────────────── + + 'dot-grid' => array( + 'width' => 24, + 'height' => 24, + 'paths' => array( + array( 'd' => 'M10.8 12a1.2 1.2 0 1 0 2.4 0 1.2 1.2 0 0 0-2.4 0z' ), + ), + ), + + 'cross-grid' => array( + 'width' => 24, + 'height' => 24, + 'paths' => array( + array( 'd' => 'M9 11.5h6v1H9zM11.5 9h1v6h-1z' ), + ), + ), + + 'diagonal-lines' => array( + 'width' => 6, + 'height' => 6, + 'paths' => array( + array( 'd' => 'M5 0h1L0 6V5zM6 5v1H5z' ), + ), + ), + + 'horizontal-lines' => array( + 'width' => 24, + 'height' => 24, + 'paths' => array( + array( 'd' => 'M0 11h24v2H0z' ), + ), + ), + + 'diagonal-stripes' => array( + 'width' => 40, + 'height' => 40, + 'paths' => array( + array( 'd' => 'M0 40L40 0H20L0 20M40 40V20L20 40' ), + ), + ), + + 'dashes' => array( + 'width' => 20, + 'height' => 20, + 'paths' => array( + array( 'd' => 'M6 9.5h8v1H6z' ), + ), + ), + + 'polka-dots' => array( + 'width' => 20, + 'height' => 20, + 'paths' => array( + array( 'd' => 'M0 3a3 3 0 1 0 6 0a3 3 0 1 0-6 0zM10 13a3 3 0 1 0 6 0a3 3 0 1 0-6 0z' ), + ), + ), + + // ── GEOMETRIC ──────────────────────────────────── + + 'hexagons' => array( + 'width' => 28, + 'height' => 49, + 'paths' => array( + array( 'd' => 'M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z' ), + ), + ), + + 'diamond-grid' => array( + 'width' => 20, + 'height' => 20, + 'paths' => array( + array( + 'd' => 'M10 0l10 10-10 10L0 10zM10 2l8 8-8 8-8-8z', + 'fillRule' => 'evenodd', + ), + ), + ), + + 'chevrons' => array( + 'width' => 28, + 'height' => 12, + 'paths' => array( + array( 'd' => 'M0 12L14 0l14 12v-2L14 2 0 10z' ), + ), + ), + + 'flipped-diamonds' => array( + 'width' => 16, + 'height' => 32, + 'paths' => array( + array( + 'd' => 'M8 0l8 16-8 16L0 16 8 0zm0 4L2 16l6 12 6-12L8 4z', + 'fillRule' => 'evenodd', + ), + ), + ), + + 'triangle-grid' => array( + 'width' => 40, + 'height' => 40, + 'paths' => array( + array( + 'd' => 'M0 40L20 0l20 40zM4 38L20 4l16 34z', + 'fillRule' => 'evenodd', + ), + ), + ), + + 'falling-triangles' => array( + 'width' => 36, + 'height' => 72, + 'paths' => array( + array( + 'd' => 'M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z', + 'fillRule' => 'evenodd', + ), + ), + ), + + 'glamorous' => array( + 'width' => 180, + 'height' => 180, + 'paths' => array( + array( 'd' => 'M81.2792408,88 L68.4142136,88 L87.7117544,107.297541 L81.2792408,88 L81.2792408,88 Z M83.3874259,88 L96.6125741,88 L90,107.837722 L83.3874259,88 L83.3874259,88 Z M98.7207592,88 L111.585786,88 L92.2882456,107.297541 L98.7207592,88 L98.7207592,88 Z M65.7928932,85.7928932 L73.5857864,78 L75.2071068,78 L105.792893,78 L106.414214,78 L106.914214,78.5 L114.207107,85.7928932 L115.414214,87 L90.7071068,111.707107 L90,112.414214 L64.5857864,87 L64.5857864,87 L65.7928932,85.7928932 L65.7928932,85.7928932 Z M68.4142136,86 L74,80.4142136 L79.5857864,86 L68.4142136,86 L68.4142136,86 Z M84.4142136,86 L90,80.4142136 L95.5857864,86 L84.4142136,86 L84.4142136,86 Z M100.414214,86 L106,80.4142136 L111.585786,86 L100.414214,86 L100.414214,86 Z M92.4142136,80 L103.585786,80 L98,85.5857864 L92.4142136,80 L92.4142136,80 Z M82,85.5857864 L87.5857864,80 L76.4142136,80 L82,85.5857864 L82,85.5857864 Z M17.4142136,0 L0.707106781,16.7071068 L0,17.4142136 L0,0 L17.4142136,0 L17.4142136,0 Z M4.27924078,0 L0,12.8377223 L0,0 L4.27924078,0 L4.27924078,0 Z M14.5857864,0 L2.28824561,12.2975408 L6.38742589,0 L14.5857864,0 L14.5857864,0 Z M180,17.4142136 L162.585786,0 L180,0 L180,17.4142136 L180,17.4142136 Z M165.414214,0 L177.711754,12.2975408 L173.612574,0 L165.414214,0 Z M180,12.8377223 L175.720759,0 L180,0 L180,12.8377223 L180,12.8377223 Z M1.42108547e-14,163 L15.7928932,163 L16.4142136,163 L16.9142136,163.5 L24.2071068,170.792893 L25.4142136,172 L17.4142136,180 L0,180 L0,163 L1.42108547e-14,163 Z M0,173 L6.61257411,173 L4.27924078,180 L0,180 L0,173 Z M14.5857864,180 L21.5857864,173 L8.72075922,173 L6.38742589,180 L14.5857864,180 Z M2.4158453e-13,165.414214 L5.58578644,171 L0,171 L0,165.414214 L2.4158453e-13,165.414214 Z M10.4142136,171 L16,165.414214 L21.5857864,171 L10.4142136,171 Z M2.41421356,165 L13.5857864,165 L8,170.585786 L2.41421356,165 Z M180,163 L165.207107,163 L163.585786,163 L155.792893,170.792893 L155.792893,170.792893 L154.585786,172 L162.585786,180 L180,180 L180,163 Z M165.414214,180 L158.414214,173 L171.279241,173 L173.612574,180 L165.414214,180 Z M180,173 L173.387426,173 L175.720759,180 L180,180 L180,173 Z M158.414214,171 L164,165.414214 L169.585786,171 L158.414214,171 Z M180,165.414214 L174.414214,171 L180,171 L180,165.414214 Z M172,170.585786 L177.585786,165 L166.414214,165 L172,170.585786 Z M152.932504,25.6532829 L154.346717,27.0674965 L120.405592,61.008622 L118.991378,59.5944084 L152.932504,25.6532829 Z M154.346717,152.932504 L152.932504,154.346717 L118.991378,120.405592 L120.405592,118.991378 L154.346717,152.932504 Z M27.0674965,154.346717 L25.6532829,152.932504 L59.5944084,118.991378 L61.008622,120.405592 L27.0674965,154.346717 Z M25.6532829,27.0674965 L27.0674965,25.6532829 L61.008622,59.5944084 L59.5944084,61.008622 L25.6532829,27.0674965 Z M0,85 C2.209139,85 4,86.790861 4,89 C4,91.209139 2.209139,93 0,93 L0,85 Z M180,85 C177.790861,85 176,86.790861 176,89 C176,91.209139 177.790861,93 180,93 L180,85 Z M94,0 C94,2.209139 92.209139,4 90,4 C87.790861,4 86,2.209139 86,0 L94,0 Z M94,180 C94,177.790861 92.209139,176 90,176 C87.790861,176 86,177.790861 86,180 L94,180 Z' ), + ), + ), + + 'architect' => array( + 'width' => 100, + 'height' => 199, + 'paths' => array( + array( 'd' => 'M0 199V0h1v1.99L100 199h-1.12L1 4.22V199H0zM100 2h-.12l-1-2H100v2z' ), + ), + ), + + 'hideout' => array( + 'width' => 40, + 'height' => 40, + 'paths' => array( + array( + 'd' => 'M0 38.59l2.83-2.83 1.41 1.41L1.41 40H0v-1.41zM0 1.4l2.83 2.83 1.41-1.41L1.41 0H0v1.41zM38.59 40l-2.83-2.83 1.41-1.41L40 38.59V40h-1.41zM40 1.41l-2.83 2.83-1.41-1.41L38.59 0H40v1.41zM20 18.6l2.83-2.83 1.41 1.41L21.41 20l2.83 2.83-1.41 1.41L20 21.41l-2.83 2.83-1.41-1.41L18.59 20l-2.83-2.83 1.41-1.41L20 18.59z', + 'fillRule' => 'evenodd', + ), + ), + ), + + 'brick-wall' => array( + 'width' => 42, + 'height' => 44, + 'paths' => array( + array( + 'd' => 'M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z', + 'fillRule' => 'evenodd', + ), + ), + ), + + // ── ORGANIC ────────────────────────────────────── + + 'topography' => array( + 'width' => 600, + 'height' => 600, + 'paths' => array( + array( 'd' => 'M600 325v-1l-15 9q-15 9-24 19-8 9-23 14l-5 2-4 2q-14 4-19 10-7 6-17 8l-7 2-6 2q-7 1-10 4l-13 5-5 1-5 1-11 5q-4 1-9 8l-6 9-10 15-5 4-12 9-3 4q-2 2-3 9c-2 10-4 14-8 22l-12 14-3 4-5 5a42 42 0 0 0-10 17q-4 11-2 27l1 8 1 37h1l-2-45q-2-16 2-27a41 41 0 0 1 9-17l5-5 4-3 1-2q5-3 10-13c5-7 7-11 9-22l2-9 4-4 12-8 4-5 11-14 6-9q5-7 8-9l11-4 8-2h2l13-6 11-3 6-2 6-2q11-3 17-9 5-4 19-10l5-1 5-2q15-6 22-15 9-9 25-19zv-64q-6 12-13 17l-9 7-6 5-1 1-5 4-18 13-8 8c-6 6-11 8-29 10q-15 2-31 12l-13 7q-9 4-16 4c-17 0-41 12-52 23-12 14-31 28-45 31q-11 4-14 15l-1 18-1 13q-3 9-11 19c-13 17-22 47-22 72v9l-1 22q0 16-5 29h-1q5-13 5-29l1-22v-9c0-25 9-55 23-72q8-10 10-19l1-13v-4l1-14q3-13 15-15c13-4 33-18 45-31 10-12 35-23 52-23q8 0 15-5l7-3 7-4q15-10 31-12c17-2 23-4 29-10q5-6 8-7l17-14 5-3 1-1 7-5q5-5 9-7 7-6 13-20zm0-116h-2q-9 1-22 7-5 3-14 3l-19-2-4-1-17-1q-16 0-18 3 0 2 4 5l4 4 4 4 6 2 2 1q6 0 7 2 2 3-1 9-3 10-10 10-4 1-14-5-7-3-9-15v-5q-2-10-4-11l-3-1h-21c-21 2-29 6-38 15l-4 6-2 3-3 3-12 13-4 3-5 4-17 15q-11 13-11 16-1 2 6 3h3l7 2q11 6 14 11 5 9-9 20-11 10-11 15v5q1 8-3 17l-4 8q-4 5-10 7l-5 2-18 10q-12 10-16 25l-2 16-5 16c-2 4-11 27-13 29l-6 14-11 17q-7 9-13 29l-2 5-5 12c-6 16-25 26-46 26q-16 0-31-8c-6-3-5-2-22-15-15-9-18-10-21-6l-2 2-5 6-6 8q-5 3-6 7l-6 7-2 11-1 3h-1l1-3q1-8 3-11l5-8 6-7 7-8 5-5 1-3c4-5 8-4 22 7l22 14q15 9 31 8c21 0 40-10 46-25l4-12 2-6q6-20 14-28l10-18 7-13c1-3 10-25 13-29l4-16 2-16q4-15 17-25l13-9 5-2 4-2q7-1 10-7l5-7q4-9 2-17v-5q-1-7 12-15 13-12 8-19-3-6-13-12l-7-1h-3q-7 0-7-4-1-3 12-16l21-20 4-3 2-1 11-11 2-4 2-3 5-5c8-10 17-14 37-15h23l2 1q3 1 4 10l1 3v3q3 11 8 15 10 5 14 4 6 0 10-9v-8q-1-2-6-2l-2-1q-4-1-6-3l-5-4-4-3q-4-4-3-6 2-4 18-4l17 1 4 1 4 1 15 1 14-2a70 70 0 0 1 24-8zm0-101v1q-13 26-24 36-4 5-13 6h-40l-15 3h-2c-13 0-20 2-33 5l-14 4-33 4-20 1q-8 0-18 5-12 7-20 24l-4 10q-5 8-13 12l-4 2-16 10c-8 8-13 17-22 36l-2 4q-10 23-18 30-10 13-40 20l-35 10c-10 5-19 11-36 27l-2 2-32 24-12 8q-17 11-25 20l-6 9q-5 9-9 25l-2 7-2 6c-2 11-6 17-14 27l-4 5-5 5-3 7q-2 3-2 12 1 13-5 22l-6 16-4 7q-3 9-13 17-8 10-10 16l-2 5v2h-1v-2l2-5q3-7 11-16l13-17 3-7 7-16c3-8 4-13 4-23l2-12 4-6 4-6 4-5a65 65 0 0 0 16-33l2-6q4-16 9-25l7-9q8-10 24-21l13-8 31-24 2-2c18-16 26-22 37-26q6-3 16-5l19-6q29-7 40-19c5-7 10-14 17-30l2-4c9-20 14-29 23-36l15-11 4-2q9-4 13-12l5-9q6-18 20-25 9-5 18-5l20-1 33-4 14-3c13-4 20-5 33-6h2l15-2q6-3 22-1h18q8-1 13-5 10-12 24-37m0 450-3-20-1-2v-2q-2-22-5-25-4-3-3-8l1-2v-7q-2-4-8-7-8-2-5-8l3-5 6-6q9-9 9-16a15 15 0 0 1 6-13v1a14 14 0 0 0-5 12q0 7-10 17l-8 10q-2 5 4 8 8 2 9 6v8l-1 2q0 4 2 8 4 2 6 25v2l1 2 2 16zm0-62-5-11-1-5 2-6 4-11v4l-3 8-2 5 1 4 4 10zm0-85-6 4-21 8q-13 7-23 6l-15-6-3-2-5-4q-5-3 1-10l19-14 4-3 23-14q16-13 26-15v1q-9 2-26 14c-4 4-4 4-22 15l-5 3-18 14q-6 6-2 9l6 3 3 2q8 5 14 6 9 1 23-5l20-9 7-5zm0-41zq-9 0-20 9l-4 3-5 3-6 4-9 6-8 6q-7 5-8 9-1 5 7 6 16 1 31-5l17-12 5-8v2l-5 6-17 13q-15 7-31 4-9-1-8-6 2-4 8-10l18-12 6-3 4-4 5-3q10-9 20-9m0-155zl-8 1-30 10h-2q-12 1-12 6l8 26 3 7 1 4q2 11 5 12l8-1q5-3 11-10l4-5 4-4 8-21v4a67 67 0 0 1-11 21l-4 6q-6 7-12 10h-8q-4-1-6-12l-1-3-2-8q-8-19-8-26-1-6 12-7h2l11-3zM0 292q15-17 23-35l11-31 1-5 15-39q9-20 21-26 9-6 12-13l2-7 4-10q3-6 12-9l18-9V94c0-10 0-11 2-13l2-1 3-2 9-6 8-8 9-8q6-3 11-2l19 2 22 1q7-1 15 2l13 1q6 0 10-3 8-5 12-5c3-1 4-1 16 2q26 6 41-10 7-7 4-14l-1-5q-1-3 2-7 4-4 7-18h2q-3 15-7 20-3 2-2 5l1 4q3 9-4 17-16 16-43 10c-12-2-13-3-16-2q-4 0-10 5l-12 3-13-1q-8-2-15-2l-41-3q-7-3-18 10l-8 8-10 6-3 1-1 1c-2 2-2 3-2 12v9q1 4-1 6c-1 3-2 3-19 10q-8 3-11 8-2 3-3 10l-2 7q-3 8-14 14-10 6-19 25c-5 9-8 19-15 39l-1 5-11 30q-8 20-25 38zm0 78 1-1q9 1 5 14l-6 11v-2l5-9q3-12-4-13zv-47l14-8c13-10 30-38 30-54 0-18 21-73 32-79 12-9 32-28 36-38q1-6 14-10 9-1 11-4l4-11 2-8q6-18 14-20l10-4q10-5 17-5 9-1 19 5 9 4 20 4l20-3 9-2 5-1 37-2 22-4c19-7 32-19 33-28l3-11q5-11 3-18l-1-7-4-16h1l3 16 2 7q2 7-3 18l-2 11c-2 10-16 22-35 28q-9 4-22 5l-42 3-9 2q-10 3-20 3-12 1-20-5-9-5-19-4-7 0-17 5l-10 3q-8 2-13 20l-2 9-5 10q-1 4-12 6-10 1-13 8c-3 10-23 30-36 38-10 7-31 61-31 79 0 16-17 45-31 54l-14 9v-1zm0-174zq11-6 22-19l16-20 7-7 6-4q13-10 16-19l2-12 3-16q3-25 10-32c10-14 20-28 20-33 0-3 0-3 6-16l5-17h1q0 5-4 14l-2 4-6 15c0 5-9 20-20 33q-6 7-9 32l-3 16-3 12q-2 11-16 20l-5 4-8 6-15 21q-11 13-23 19m0 362v-4l2 6q2 9 8 7c19-9 34-25 34-40a78 78 0 0 1 22-52l2-2 2-2q5-2 8-12l4-13v-2q6-14 5-19-1-6-12-2l-9 4q-20 7-43 6h-3l-15 2-5 3v-1l5-2 16-3h2q23 1 43-6l9-3q12-6 13 2 1 5-5 19v2l-5 13q-1 10-7 13l-2 2-2 1-6 6a77 77 0 0 0-16 46c0 16-15 31-35 40q-6 3-9-7zm0-62v-2l1 3q4 7 5 17l1 18 1 15q2 4 9-1c17-15 22-23 22-39q0-10 3-18l14-19 7-9 4-3 1-1 2-3 3-6v-3l2-5-1-4q-2-3-24 4-12 5-28 5-9-1-13 2-5 3-8 14l-1 6v-4l1-2q2-12 8-15 3-3 13-2c12 0 18-1 28-4q22-8 25-5l1 5-1 4-1 2v1l-1 1-2 6-3 4-1 1h-1l-2 3-8 8c-9 13-11 15-13 20q-3 7-3 17c0 16-5 25-23 39q-7 7-10 2l-1-16-1-18-4-17zM379 0h1l2 5q5 17 5 28 2 16-7 31-11 23-36 34-19 9-36 10l-37 4h-3l-37 5-20-2-9-2q-30-5-31 8l-1 3v3l-4 20q-4 8-16 8-13-1-15 3l-9 8-15 14-7 6a92 92 0 0 0-26 40q-4 14 19 2l44-22 16-7 11-5 2-2 2-1 5-2q26-15 50-1c23 13 39 37 39 56s-1 25-6 33l-5 7q-10 13-12 22-3 10 20 0 19-10 28-27l6-15 4-12 2-5c7-15 21-31 36-37q10-5 19-21l5-9q6-10 10-12 16-11 50-11 12 0 28-4l14-4 20-4c14-4 41-5 64-5 17 0 28-10 39-39 6-18 6-27 2-35l-1-2-1-6 2-25 3-30-1-3h1l1 3-3 31-2 24 1 6 1 2c4 8 4 17-3 35-10 30-21 40-39 40a340 340 0 0 0-83 9l-15 4q-16 4-28 4-33 0-50 10l-9 12-5 9q-8 17-20 21c-15 7-28 22-36 37l-1 5-4 12-6 16q-9 17-29 26-24 11-21-1 2-8 13-21l4-7c5-8 6-14 6-33s-16-42-38-56q-24-13-50 2l-7 3-2 1-11 6-15 7-45 21q-23 13-20-2a84 84 0 0 1 26-41l7-6 16-13 8-9q3-4 16-3 11 0 15-8 3-5 4-20v-2l1-3q0-8 11-10 7-1 22 1l28 5 37-5 3-1 37-4q17 0 36-10 23-10 36-33 8-15 6-31 0-11-5-27zm14 0zl3 15q6 21 0 39a61 61 0 0 1-12 20l-19 19q-16 14-36 18l-22 5-11 1-9 1q-17 2-22 4a144 144 0 0 1-53 6l-9-2-13-3q-6 1-9 7l-3 15q0 12-10 14l-7 3-1 1-2 1q-2 2-9 2l-10-1-7 6-6 6-8 6-11 9q-7 7-6 13 3 5 9 3l8-4 10-5 12-5 11-5 13-7 12-5q25-11 37-6l6 2 6 2 13 6q27 14 34 40c7 28 4 45-8 62q-5 9 0 9 8-1 16-12 4-6 8-22l1-6 2-7q7-24 22-34l25-17q10-7 15-18 10-25 44-24 13 0 30-4l6-1 15-2 31-6 3-1a170 170 0 0 1 56-6q24-1 30-14l3-9 3-6c5-13 6-22-1-40l-6-21V2l1-2h1l-1 2v40l5 21c8 18 7 27 2 40l-3 6-4 9q-4 14-30 15l-24 1q-8 0-31 5h-3a172 172 0 0 1-41 8l-6 1-6 1q-17 4-30 4-33-1-44 23-4 12-14 19l-26 16q-14 11-21 35l-2 6-1 6q-4 16-9 22-8 13-15 13-8 0-1-10c11-17 14-34 8-62q-8-26-35-39l-19-8-5-3q-12-4-37 7l-12 5-13 7-11 4-12 6-9 5-8 4q-9 2-10-4-3-5 5-14l11-9 8-6 7-5 7-7 10 1q7 0 9-2l1-1 2-1 7-2c6-3 8-5 9-14l4-15q3-7 9-8l13 3 9 2 4 1q34 0 49-6 5-3 22-5l9-1 33-5q18-5 35-19l20-18a60 60 0 0 0 11-21q6-17 1-38zm114 0zl-11 11-5 2q-3 0-7 2l-2 1q-6 5-14 5t-16-5l-2-1-3-1-4-1-3-1q-2 1-11-3h-2l-10-3q-4 0-4 6c0 21-6 50-13 57a224 224 0 0 1-78 54l-13 4-11 3-12 2-14 2c-21 5-25 6-38 6l-14-2-6-1q-12-3-18-1-7 3-4 11 3 7 9 11l14 3 16 3 18 8q13 6 20 20l11 22q4 8 11 2 6-7 21-14l2-2 16-9q7-6 11-13 13-31 32-33h6l24-1q21 0 30-4l37-14 15-4 11-4 8-3 7-3 6-3q4-4 12-5h1q8-1 13-5 10-12 4-45-3-12-2-35l1-19h1v54q9 34-4 45-5 5-13 6h-1l-12 4-6 4-6 3-9 3-11 3-82 23-25 1h-5q-17 1-32 32-2 8-11 14l-16 9-2 1-21 14q-7 9-12-2l-11-21q-7-13-19-21l-19-7-15-3q-10-1-14-4-6-2-9-11-4-9 4-12 6-2 18 1l6 1 14 2c13 0 17-1 38-5l37-7 13-4a223 223 0 0 0 78-54c6-7 12-37 12-57q0-7 5-7l11 3h1q9 4 11 3l4 1 4 1 3 2 1 1q9 4 16 4t13-4l3-2q3-3 7-3 2 1 4-1 4-1 10-9zM325 600h-2l1-29a112 112 0 0 1-2-40l5-18 13-19 12-10 7-8 1-3-1-9v-9q-2-18 8-22 23-6 33-25c10-17 26-32 36-32q17 1 29-9 9-7 13-7l9-1 6-3 5-5q8-9 21-9h7l12-1c3-1 5-3 14-11a130 130 0 0 1 27-19l9-7 12-12v3l-10 10-14 10c-12 8-14 10-22 17-10 8-12 10-16 11l-12 1h-7q-12 0-19 8l-6 6q-4 3-7 3l-9 1q-3 0-11 7-13 10-31 9c-9 0-25 14-34 30q-11 20-35 27-7 3-6 20v9l1 9-1 4-8 8-12 11-12 17-5 18a107 107 0 0 0 2 39l1 4-1 17zm-14 0zq5-9 5-19l-1-16-1-28a118 118 0 0 1 11-43q4-11 10-17l4-4 4-4c6-5 8-11 8-29 0-19 2-24 15-28l3-1 6-3q8-4 15-11c25-28 37-35 56-35q12 0 22-7l18-11 9-4c8-4 14-5 26-5q11 0 15-2 6-2 11-8 3-5 14-12l21-13q7-6 13-15l6-9v2l-5 7q-5 10-14 15l-15 10-5 4q-11 6-15 11-4 8-11 9-5 2-15 2c-12 0-18 1-26 4l-8 5-19 11q-10 7-22 7c-19 0-30 7-56 34a54 54 0 0 1-26 17q-7 2-9 6-3 6-3 20c0 18-2 24-8 30l-4 3-4 5q-5 6-11 16l-4 11q-4 15-6 32a230 230 0 0 0 2 44q0 8-5 19m25 0zl1-15-3-24v-2q-3-18-2-27 1-13 12-22c22-19 29-31 29-45v-8q-2-6-1-8l3-6q4-4 14-7c10-4 15-10 24-22l2-3 10-12q7-6 15-8l25-8 5-3 5-2q2-3 11-3l11-1 10-6q6-8 17-9l4-1 14-3 23-20 3-3q14-13 22-16l11-9v1q-5 6-11 8l-21 17-3 3c-17 15-19 17-24 19q-4 3-14 4l-3 1q-11 1-18 8l-10 7-11 1q-8 0-11 2l-4 3-5 3-26 7c-10 3-15 8-24 21l-2 3q-12 17-25 21l-13 7-3 6v8l1 8c0 15-7 27-30 45q-10 9-11 22-1 9 2 27v1q3 18 2 25zm-240 0h-2l2-22q1-25 11-33 7-6 9-16l1-9 3-10q1-5 7-5c14 0 38 9 54 20 8 7 21 14 31 17q20 8 25-1l3-6q4-10 5-18v-6q0-3-4-7l-1-2-3-2-2-4-1-3-5-5q-7-6-28-7l-9-1h-2q-14-1-19-3c-14-6-35-26-35-37q0-8 5-14l13-14 18-14 17-12 7-5 7-8q10-10 20-14l17-5 12-4 13-3q31-10 40-19c9-8 12-14 16-26l5-12c10-18 29-40 44-46q20-9 19-18 1-5 7-14 7-9 17-14c12-6 33-10 47-10q7 1 16-5c10-5 11-5 14-5 10 0 32-3 46-6l4-1 3-1 16-2 8 1q9 2 23-6l10-6v3c-11 8-26 13-33 11l-8-1-10 1-9 2-4 1c-14 3-35 6-46 6-3 0-4 0-13 5q-9 5-17 5c-14 0-34 4-47 10l-16 13-6 13q1 11-21 20c-14 6-33 27-42 44l-5 13c-4 12-7 18-16 27q-11 9-42 19l-13 3-11 4-18 5q-9 4-19 14l-7 7-6 6-18 11q-11 8-17 15l-13 13q-5 6-5 13c0 9 20 29 33 35q5 2 19 3h4l7 1q21 1 30 7l4 6 1 3 3 3 2 3 1 1q5 4 5 9v7q-1 8-5 18l-3 5q-6 11-27 3-18-7-33-17c-14-11-38-20-52-20q-6-1-8 13l-1 10q-2 11-9 17-10 7-11 32l-1 5zm8 0h-1l1-5 1-5q3-19 7-24 1-4 8-11 11-15 10-19v-5q1-9 5-10 6-2 20 7l26 15 18 11 6 4c14 8 39 5 47-8l7-18 2-3 4-7q14-21 14-37 0-4 4-15l1-4q6-17-4-24l-10-3q-10-2-24 6l-8 3-4 3-4 2q-17 11-33 6l-13-2q-18-4-24-7-9-5-2-16l14-19 8-7 8-7 7-6q9-13 20-19 14-11 35-15c37-9 51-13 59-18q12-9 37-55 8-15 21-26l17-12 12-8 3-5 7-15q9-17 31-20a119 119 0 0 1 35-3l17-3a152 152 0 0 1 69-12h28l14-6v1l-14 5q-9 3-25 1h-13q-29 0-59 11a45 45 0 0 1-22 4c-10 0-17 0-30 3q-21 3-31 20l-4 7-3 7-3 5-11 9-18 12q-12 10-21 25-23 46-37 55c-8 6-22 10-59 18a96 96 0 0 0-55 34l-6 7-8 7-8 7-15 18q-6 10 3 16 5 3 23 6l13 3q17 4 32-6l5-3 4-2 7-4q14-8 25-6l10 4q11 7 5 25l-1 3c-3 9-3 11-3 15q0 16-16 37l-4 8-1 3c-3 9-4 11-8 17-7 14-33 17-47 8l-7-3-17-11-26-16q-15-7-19-6-4 2-4 9l-1 3 1 2c0 5-3 9-12 20l-8 10q-3 5-6 24l-1 6zm10 0zv-1c0-12 9-34 20-43l3-5 3-4 2-5c4-6 7-6 17 1l7 4 20 13q21 14 34 14c17 0 33-6 40-15l5-10 2-7 3-7q7-17 12-23c8-9 14-36 14-53l2-8v-5q1-9-1-13-9-11-39-5l-19 7-16 9q-9 6-25 1c-30-7-31-8-15-26 17-21 47-46 56-46 10 0 54-12 66-18 9-5 24-22 37-38l19-30q14-20 27-27 10-7 19-19l12-21q3-2 5-3c14-7 24-9 39-9h29l23-3 4-1 28-1c19 0 29-1 45-4l9-4v1l-9 3a174 174 0 0 1-51 5l-22 1-4 1-23 3h-29c-15 0-25 2-39 8l-4 4-13 20q-7 13-19 19-12 8-26 28l-20 29c-12 17-27 34-37 38-12 6-56 19-66 19-9 0-38 25-56 45-14 18-14 19 15 26q16 4 24-2l17-9q7-4 19-6c19-5 35-3 39 5q3 4 2 13v5l-2 8c0 17-7 44-15 53l-11 23-3 8-8 16c-7 9-23 16-40 16q-13 0-35-14l-13-9-7-4-6-5q-13-9-16 0l-2 4-3 5-4 4a70 70 0 0 0-19 43zm-29 0h-1l1-8c0-20 5-42 13-49l10-13q3-8 3-21 1-11 4-15l9-8 4-2 7-10q3-6-1-16l-4-8-1-2-3-5q-9-14 8-29l26-22 20-12 7-7 23-18 13-6 50-16 7-3c9-3 12-4 17-8q10-9 20-28c16-35 19-40 32-49l11-7 2-1q16-6 22-17l3-12q1-8 3-11 4-4 15-9l12-3c16-5 25-6 39-6l13-2 4-2q5-4 13-6l19-2h2l20-4 19-5 4 1 5 1q13 2 25 0 12-4 20-15v2q-8 10-20 14-12 2-26 0l-5-1-3-1-20 5-19 4h-2l-19 2-13 5-3 3-14 2c-14 0-23 1-39 5l-12 4q-10 5-14 9-2 3-3 10l-4 12-9 12-12 6-2 1-12 6c-13 10-16 15-32 49q-9 21-19 28-6 6-17 9l-8 2q-9 6-28 10l-22 6q-6 2-12 7l-13 10-10 8-8 6-19 13-27 21q-15 15-7 29l2 5 1 2 4 8q5 10 2 17-1 4-8 9l-5 4q-6 3-7 6-4 5-4 15 0 14-4 21-2 7-10 13c-7 7-12 29-12 49zm282 0zl-1-6q-4-22-3-37l1-15v-8q1-6 11-16c9-11 12-16 19-30l7-11q4-6 16-17 16-13 19-19l1-5 2-4c4-10 8-14 23-17l21-6q23-5 34-12l27-17 3-1 23-14 9-7 6-5 16-10v1l-15 10-7 5-9 6-23 14-2 2-28 16q-10 8-34 13l-21 5q-12 4-16 8-4 3-6 10l-2 4-2 4c-2 6-7 10-18 20q-14 11-17 16l-2 5-4 6c-7 15-10 20-20 30q-9 11-10 16v23q-3 15 2 37zm-76 0h-2l1-4 3-11c2-12 3-23 3-41q-1-9-4-10-5 0-11 17-14 43-43 42c-22 0-29-1-48-10l-10-5-15-10q-6-4-9-1l-2 2-5 4-3 2-1 1q-11 8-11 21v3h-2v-3c0-11 3-16 12-23l2-1 2-2 4-4 3-2q5-2 11 1l15 10 9 5c19 9 26 10 48 10q27 0 41-40 7-19 13-19 6 1 6 12a191 191 0 0 1-7 56m-149 0zl7-13q3-9 9-11 5-1 12 5l8 6 9 2 14 6 17 6h-4l-5-2-9-3-13-6-9-3q-2 1-8-5-7-6-12-5t-9 10zm112 0 12-6 5-7 3-5 3-3v-1l1-2 4-5 2-2 1 2 1 2-1 16-3 11h-1l3-11 1-16-1-2v-1l-2 2-3 5-1 1v1l-3 4-9 11-9 6zm-183 0h-2q5-4 5-15c0-19 0-17 2-21l3-6 7-11 12-19q4-7 4-24 0-11 9-23l2-5 3-4 3-9q0-5-5-11-8-7-12-1-2 3-5 16v5l-3 9q-1 7-8 12l-3 4q-10 8-12 19l-1 6-2 10q-5 18-23 44-11 17-11 24h-1q0-7 12-24a137 137 0 0 0 24-55l1-5q3-11 12-20l3-3 9-12 2-10v-4q2-13 6-17 4-6 12 2 6 5 6 11l-3 9-3 4-3 5q-8 12-8 23 0 16-4 25l-13 18-6 11-4 7-1 20q0 10-4 15M310 0h1l-1 3q-6 12-6 25 1 12-6 13h-18q-15-3-28-1c-6 3-26 7-32 7-11 1-37-2-54-7a22 22 0 0 0-17 3q-5 2-14 12-8 9-14 11-10 6-13 13-3 5-2 15 0 6-10 10l-4 1q-13 6-12 17c0 13-5 19-18 28l-4 3-8 5q-10 7-15 34l-4 17-6 19-4 17-3 6-13 18-5 9v-2l5-7 12-18 3-7 5-16 5-19 4-18q5-27 16-33l7-6 5-3q18-10 17-27 0-12 13-17l4-2q9-4 8-9 0-11 3-16 3-7 14-12l14-12q8-10 13-12a23 23 0 0 1 18-2c17 4 43 6 54 6 6 0 26-4 32-7q12-2 28 1h18q6-1 6-12c0-10 1-16 6-28m25 0h1l1 41q0 15-13 23-10 8-24 9l-23 1-20 1-5 2a70 70 0 0 1-47-1l-4-2c-12-5-21-5-37 1q-16 8-22 15-4 6-6 16l-4 12q-2 6-7 7l-9 3q-12 1-16 9-4 12-14 20l-16 13q-12 11-18 32l-4 11-3 10a181 181 0 0 1-45 85v-1l9-12 10-11c8-8 17-32 26-61l2-10 4-11q6-22 19-32l16-14q9-8 14-19 2-6 7-8 2-2 9-2l9-2 7-7 3-12q2-11 6-16 6-9 23-15c16-7 25-7 38-2l3 2q12 4 22 4 16 0 24-3l6-2 8-1h35q14-3 24-10 13-8 13-22zM94 0h2l-3 9-5 14-2 5q-3 6-13 14-12 9-20 31l-10 28c-3 8-7 13-16 24l-2 3q-9 11-12 17-4 8-13 15v-3q8-6 11-12 4-8 12-18l3-3c9-11 12-16 15-23l10-28q9-23 20-33 10-7 13-13l2-5c2-8 3-9 5-13zm9 0zq-2 12-5 15l-4 8-3 10-3 5-12 14-8 9-3 13-5 21q-4 25-10 32c-7 13-10 16-37 41L0 179v-1l14-10c26-26 29-29 37-41q5-8 9-32l5-21q2-10 4-13l10-13 11-11 4-14 5-8zM84 0zq-3 8-12 12-11 8-24 8T37 41v12q1 9-2 17l-2 6-7 16-20 44-6 11v-2l6-9 25-56 2-6 4-21-1-12 2-15q3-7 11-7 13 0 24-8 8-5 11-11m282 0h1l3 18v28q-1 14-7 21c-11 17-39 30-66 30h-16l-25 3c-21 4-27 4-36 4q-9 1-22-3l-2-1-4-1q-11-3-22 0a16 16 0 0 0-9 9l-4 12q-2 10-4 13a16 16 0 0 1-10 8l-9 4q-9 4-14 10l-6 8-8 9-13 11q-11 9-17 19-5 12-11 34l-2 12-2 5-1 6a151 151 0 0 1-31 67l-14 11-14 6-1 1v-1h1l14-7 3-2 11-8a139 139 0 0 0 30-67l1-6 4-18q5-22 12-33 6-10 16-20l13-10 9-9 5-8q5-7 14-11l10-3a15 15 0 0 0 9-8q3-4 4-13 2-10 4-13a17 17 0 0 1 10-8q11-4 22-1l4 1 4 1q11 3 20 3c9 0 15 0 36-4l25-3 16-1c26 0 54-12 66-28q4-7 6-21V22l-1-5c1-4 0-7-2-17M19 0zl-6 16-2 15 2 16 2 12q2 14-3 23L0 109v-2l13-25q4-10 2-22l-2-13-2-16q0-9 3-15zm280 0-5 11-4 12-1 2v1l-1 3-5 1-44 2-24 3-8-2c-18-4-46-5-55-3l-9 6-7 6-11 9-9 8-4 4q-8 6-12 7l-5-1q-2-1-1-3 0-5 13-25a109 109 0 0 0 14-41h1l-3 15q-3 12-12 26-12 20-12 25v2l4 1q5-1 12-6l5-4 9-8 10-10 7-6q6-5 10-5c9-3 37-2 55 3l8 1 23-3a176 176 0 0 1 45-2l4-1 1-2v-1l1-2q0-5 5-12zm-8 0c-6 18-10 22-20 22l-29 2h-1l-29 1-29-3-1-1-29-3q-5 0-11 3l-2 2-7 2q-4 0-4-4v-4l1-6 2-11h2l-2 12-1 5v4q-1 2 2 2l6-2 2-1q5-4 12-4l29 4h1a228 228 0 0 0 58 2h1l29-2c9 0 12-3 18-20zM142 0zl-3 6q-1 2 2 2l11 1 34 3 27 4c24 0 52-3 61-7q4-3 6-9h1q-2 7-7 9c-9 5-36 8-61 8l-27-4-48-7zm76 0 14 3q15 0 22-3h3a68 68 0 0 1-43 0zM71 0q-4 4-12 7L46 8h-2q-8 0-10-5V0l1 2q2 5 9 5h2l12-1q8-2 11-6zM0 208q11 0 12 11c0 11-6 28-12 42v-3q10-21 11-39 0-10-11-10zm0 7 3 1 1 8v5l-4 16v-4l3-16v-1l-1-7zm0 196v-2l3-4 5-12c7-24 7-27 0-31H0v-1l8 1c8 4 8 7 0 31l-4 12zm34 41 8-1 2-1q13-2 15 3 1 3-5 9l-5 4-6 5-3 3-3 5-7 22q-5 12-9 9-3-1-6-11l-4-8c-4-8-4-23 0-32a10 10 0 0 1 7-6l12-1zm0 2h-4c-12 0-15 1-17 5-4 9-4 24 0 32l3 7 6 10q3 2 6-7l7-23 3-6 4-3 6-4 5-4q5-6 4-8-2-3-12-1l-3 1zm345 146c-3-12-4-14-4-19v-28q3-8 10-8h26q5 0 6 8v1q1 7 9 7l6 2 1 4q-2 7 7 14 10 6 17 3 5-2 12-10l1-1 1-1 3-3 9-7 6-3q8-3 12-7l6-6q4-3 14-3 8 0 18-14l3-5 3-3q12-16 30 1 9 8 14 38l4 40h-1c0-10-1-26-4-40q-5-29-13-38-17-15-30-1l-2 4-3 4q-11 16-19 16-10 0-13 2l-7 5q-2 5-12 8l-5 3-10 6-3 4-1 2q-8 8-13 10-8 3-17-4-11-7-8-14v-3l-6-2q-9 1-10-8v-1q-1-7-5-6h-26q-7 0-9 6v28l4 19zm14 0-1-17v-3l1-11q0-8 4-8 3-1 9 3c7 4 14 13 16 19q6 16 34 16 12 0 21-8l21-20c7-6 11-8 20-9l12-2 15-11q11-11 17-5 5 6 7 21l1 35h-1a106 106 0 0 0-1-35q-1-15-7-20-5-6-16 4-7 9-15 12l-11 2q-12 0-21 8l-21 20q-8 9-21 9-28 1-34-17-6-10-17-19-5-3-8-2-3 0-3 7l-1 11v5q-1 5 1 15zm114 0 11-14q9-11 19-14 11-3 12 9l-1 19h-1v-19q1-11-10-8-9 3-19 13zm-10-339 11 8 11 6q16 9 12 25-2 10-9 15l-16 6c-12 4-14 4-19 7l-8 4q-16 10-28 3l-17-11-5-3c-17-9-35-24-35-32q0-5 6-10l4-3 4-3 9-9c11-13 27-28 35-32q16-8 27 0 5 4 9 14l2 6q3 6 5 7zm-4 0-4-8-3-7q-4-9-8-12-9-8-26 0c-7 3-22 18-33 32l-10 8-4 3-3 3q-6 5-6 9c0 7 18 22 33 30l6 3 17 11q11 7 26-3l8-4c5-3 7-3 19-7q11-3 15-6 8-4 9-13 4-15-11-23l-11-6zM88 242l12-7 3-2 4-1 18-9 28-15 21-9c19-9 28-7 49 8q12 8 17 22 4 13 3 28 1 11-3 19-5 12-26 23c-24 14-39 28-53 48-8 13-47 45-65 54-21 13-47 23-65 23q-15 0-15-14 0-9 7-24 4-10-3-22-4-10-3-13 0-5 5-11l12-12 11-10c9-12 12-20 18-45 3-15 6-20 15-25l4-3zm0 0-5 4-5 3c-8 4-11 9-15 24-5 26-8 34-18 45q-3 5-10 11l-12 12q-6 6-6 10l4 12c5 10 6 15 2 23q-6 16-6 24-1 12 14 13c18 0 44-10 65-22 17-10 56-42 65-54 14-21 28-35 53-48q19-11 25-23 4-8 4-19 0-16-4-27-5-14-16-23c-21-14-30-16-49-8l-21 10-5 2-23 12-18 9-4 2-3 2zm-7 13c7-7 12-9 23-12q8-2 13-5 4-5 16-6l5-1q7-2 13-6c10-9 25-14 36-14 5 0 17 7 26 17q16 18 12 32-5 14-23 28-13 11-25 16-5 3-10 10l-16 28q-7 13-30 28l-9 7-9 6-13 9c-11 11-37 16-43 9q-3-2-2-10 1-13-5-21l-7-7q-6-4-7-9-1-9 14-22l7-6 6-4q5-6 7-20l1-5 7-31zm0 0-12 11-1 6-3 10-2 11-1 7-1 2q-2 15-8 20l-5 5-7 6q-15 12-14 21l6 9 8 7q6 7 5 21-1 8 1 10c6 6 31 1 43-9q3-4 13-10l8-6 10-7q21-15 30-27l16-28q4-7 10-10l25-17q17-14 23-27 4-14-13-32c-9-9-20-16-25-16q-18 0-36 13-5 5-13 6l-5 2q-12 1-16 5-4 5-12 6c-12 3-16 5-24 11m131-3q0 10-3 14l-8 11q-8 11-17 14l-3 2-2 1-6 2q-9 4-9 11c0 11-19 36-35 45q-14 7-32 12-21 4-25-3-1-4-12-6l-10-2q-3-3-4-7l3-10 3-5 4-6q10-15 10-27 1-29 28-43 7-4 15-2 7 2 7-2c5-8 7-9 16-9q6 0 10-3l11-8c10-6 12-7 26-12q11-3 22 9t11 24m-2 0q0-12-10-22-11-12-21-9c-13 5-15 6-24 12l-11 7q-6 4-12 4c-8 0-10 1-14 8l-4 3h-6q-7-2-14 2-24 14-26 41 0 13-10 28l-4 6-3 5-3 9q-1 6 12 7h1q11 3 13 7c3 8 36 0 55-10 14-8 33-33 33-43q0-9 10-13l6-2 1-1 4-2q9-3 16-13l9-11q2-3 2-13m-37-23 5-3q3-1 11 4 24 13 9 38-5 9-18 11l-3 1-12 4q-6 3-8 9-11 39-36 50c-16 7-21 9-28 9q-7 2-15-3l-6-2q-6-3-4-9l5-21 1-10 2-9q1-13 4-18 4-9 12-11 7-4 18-4 7 0 9-2l2-1 3-4 3-3q4-5 12-5 5 0 8-4l4-4 7-5 5-2zm-15 9-6 5-4 4q-4 3-9 3-7 0-12 6l-2 3-4 3-2 2q-2 2-9 2-9 0-18 3-8 3-11 11-3 5-4 18l-1 5-1 4-2 10-4 21q-2 5 4 8a45 45 0 0 0 20 5c7 0 11-2 28-8q24-11 36-50 2-7 7-10l12-4 3-1q14-2 19-10 13-25-9-38-8-4-10-3l-3 1-3 2-10 5zm-42 33q23-4 32 6c5 6 2 21-6 35q-5 13-23 25c-14 11-35 13-40 4q-3-4-1-15c0-6 1-11 4-23v-1l4-14q1-11 7-14 3-3 12-2zm0 0-11 2q-8 0-12 2-4 3-7 13l-3 14v1c-3 12-4 17-4 23q-2 10 0 15c5 7 26 5 40-4q17-12 23-25c6-14 10-29 6-35q-9-9-32-6m16 6q11 0 11 8 2 5-3 16c-2 9-18 26-27 31-7 5-20 6-26 1q-6-4 0-26l2-9 3-10 1-1q2-7 10-9 7-2 18-2h4zm0 1-7-1h-4q-12 0-17 2-8 2-11 8l-1 5-4 15q-6 21-1 26c6 4 19 3 26-1 8-6 24-23 27-31q3-11 2-16 0-7-10-7m-18 7 9 1q8 3 9 10-2 15-23 21-14 6-12-16 2-9 9-14zm0 1-8 2q-6 4-7 13-3 21 10 16c12-5 22-12 22-21q0-7-9-9zm80-24c-2 10-14 10-26 3-10-5-14-18-5-22l4-3 2-1 4-3q7-4 14 0 13 6 7 26m-20-25-4 2-2 2-2 1-3 1c-8 5-4 16 5 22q20 10 26-3 5-20-7-26-7-2-13 1m18 12q0 9-7 10-5 1-11-2h-2l-2-1q-6-3 1-11c6-9 21-7 21 4m-1 0q0-7-8-9-7 0-12 5-6 8 0 10l1 1 2 1q6 2 11 1t6-9m196 281c0-13 2-16 18-29l8-6 13-6 4-1q12-5 16-12l3-7 2-4v-1l1-1q4-11 13-17l5-4 3-4 4-4q4-6 11-6 4 1 10-2l9-2q3 0 7 4l6 6q7 7 18 7l12 1q22 1 25-2 11-8 12 7-1 18-19 20-8 1-9 4-2 3 6 11 20 18-8 20a22 22 0 0 0-14 6q-5 3-10 11l-2 3-7 9q-8 9-13 7h-1l-3-1-8 7q-7 6-12 6l-6 4-5 3-5 5-4 4q-14 11-21 6-3-3-4-7v-7q2-8-2-12-3-4-3-2l-2 1q-3 4-9 3-7-1-5-10v-1l1-6h-6v1c-14 5-16 6-20 6q-9 2-9-8m2 0q0 8 7 6c4 0 5-1 19-6l1-1q6-2 8 1l-1 7v1q-2 9 3 8 6 1 7-2l2-2q3-3 7 3 4 4 2 13t4 13 17-7l5-4 5-4 4-4 8-4q4 0 11-6l2-2 2-2 5-3 3 1h1q4 1 11-6l7-10 2-2 10-12a24 24 0 0 1 16-6q25-1 6-17-8-7-6-13 3-5 11-5 15-2 17-18 0-13-8-6c-4 3-10 4-27 3h-1l-11-1q-13 0-19-7l-7-7q-2-3-5-3l-9 2q-6 2-10 2-6 0-10 6l-3 3-4 5-5 4q-9 5-12 16l-1 1v1l-2 4-3 7q-4 8-18 13l-4 1-4 2-8 4-7 5c-16 13-18 16-18 28m133-79q5 2 11 1l9-1 8-2q5-1 6 2 3 1 2 4l-5 5q-6 4-6 7 0 9 8 16 6 5 6 9-1 5-13 5-10 0-20 15l-5 7-5 5-3 2q-4 3-10 2-9 0-15 7l-4 6-9 7-15 8q-5 4-8 0l-3-11-2-9q-1-3-4-3-8 3-11 1t-3-11q1-3-3-3l-10 1h-1q-11 3-13 1-4-2 5-12 8-10 15-14 4-3 13-4 9-2 14-9l12-21q5-8 11-11l9-7 1-1q11-8 18-8 5 0 9 5l5 6zm0 0-10-7-2-3q-3-5-8-5-6 0-17 8l-1 1-10 6q-6 4-10 11l-13 21q-4 8-14 10a42 42 0 0 0-28 17q-7 10-4 11 2 2 12-1h1l10-1q5 0 5 4-1 9 2 10 1 2 10-1 3 0 5 4l2 9 3 10q1 4 7 1l15-9 8-7 5-5q5-8 15-8l10-1 3-2 4-6 5-7q11-15 21-15 11 0 12-4 0-4-5-9-9-6-8-16 0-4 5-8l5-5-1-3c-3-1-5-1-14 1l-9 1q-6 1-11-2m-28-3q7-5 13 1l3 3q6 6 14 5 15-1 16 9l2 5 2 8q4 7 2 10-1 2-5 3l-4 1-5 2-3 4-3 4q-11 13-20 12-6 1-11 4l-15 14-9 5q-6 5-9 3l-2-6v-2q0-6-7-7-10 1-10-5-1-6-6-10-6-3-2-8l12-7 11-6 5-7 3-5 2-4 3-5c5-11 10-15 23-21m-23 21-2 5-2 4-3 6-6 6q-2 3-11 7l-11 7q-3 3 1 7 6 3 7 10 0 5 9 4 8 0 8 8v2l2 5q1 1 8-2l9-6 15-13q6-5 11-5 10 1 19-12l3-4 4-3 5-3 4-1 4-2q2-3-1-10l-3-8-2-5c-2-6-4-8-15-8q-9 0-15-5l-3-3q-5-5-12-2c-12 7-17 11-23 21m56 12c-3 7-16 15-26 15q-8 0-8-7l4-11 2-2 2-3q5-8 13-8c9 0 16 10 13 16m-1 0c3-6-3-15-12-15q-8 0-12 8l-2 2-2 3q-3 3-4 10 0 6 7 6c9 0 23-8 25-14m-69 20 6 2q3 3-1 6c-5 5-13 5-13-1q1-7 8-7m0 1q-6 0-7 6 3 7 12 1 3-3 0-5zm29-225 20 9 7 6 3 5q4 6-6 11-7 6-22 10l-21 7q-10 4-25-5l-13-9-11-8-8-5q-7-4 0-13l18-16 9-8 5-4q7-9 15-4l7 5 8 8zm0 0q-7-3-15-10l-8-8q-3-4-7-5-7-4-14 3l-4 5-9 8c-14 11-15 12-18 16q-7 7-1 11l9 5 11 8 13 9q15 9 24 6l21-8q15-5 22-9 9-6 6-11l-4-5-7-5zm-39 8 8 1 9 4 13 6q13 4 9 9-3 4-18 9-12 5-21 0-8-4-12-13-4-6 0-11 4-3 12-5m0 1-12 5q-3 4 0 10l12 12q9 4 21 1l17-9q3-5-8-9l-13-5-9-4zM203 500q0 7-5 9-2 2-11 2l-1-1c-10 1-17-7-17-15s10-11 19-5q3 2 9 2 6 1 6 8m-1 0q1-7-5-7l-9-3c-9-4-19-2-19 5 1 7 8 15 17 15h1q9 0 11-2 3-1 3-8m33-117 16-5 20-5q29-7 39-13c15-10 37-34 44-48q3-8 7-6l5 1 3 1 1 1 13 3q11 1 8 8-2 7-11 12-12 9-12 34 0 5-3 8l-9 5h-1l-9 4q-8 7-20 7l-4 4-1 2q-4 5-9 5c-8 0-11 1-22 7q-7 4-14 4h-1l-11 1-9 4-29 12c-12 4-18 3-27-4q-4-2-1-7 2-4 8-9l2-2 6-5 5-8 6-7zm0 0-9 5q-4 2-6 7t-6 7l-6 6-1 2-8 8q-3 4 0 7c9 6 15 7 27 4l19-8 10-5 9-3q4-2 11-2h1q7 0 14-3c11-7 14-7 22-7q4 0 8-6l1-2q3-3 5-3 12 0 20-7 1-3 9-5h1q12-4 12-12-1-26 12-34 8-5 10-12 3-6-7-6-6 0-13-4c-8-4-7-3-9-3q-3-1-7 5c-7 14-29 38-44 48q-10 7-38 14l-21 5zm17 5 7-1 9-1q3 2 3 6 1 6-6 10l-14 7h-3l-9 6h-1c-8 6-13 9-23 9q-8-1-6-6l8-11 4-5 12-10 1-2q3-3 14-3zm0 1-4-1q-10 0-13 3l-2 1-11 11-4 4-8 11q-1 4 5 4 12 0 22-7l1-1 10-5 2-1 14-7q6-3 7-9 0-4-3-5l-9 1zm112-46c-1 6-2 7-1 13 2 7-1 16-5 16l-2 1-7-1q-10-1-10-13c0-14 2-20 8-26l2-4 3-5q6-12 12-10l4 1 1 1 3 2h11q3 2-1 5l-4 4zm0 0c1-4 3-6 14-17l4-3q3-3 1-4h-6l-5-1-3-1-2-1-3-2q-6 0-11 10l-3 5-3 4c-5 7-7 12-7 26q0 11 9 12l7 1h2c3 0 6-9 5-16q-2-7 1-13m-10 12 3 2q3 2 0 7-7 7-9-5 0-6 2-6zm-1 2-2-2-1 4q1 9 5 3 3-2 1-3zm223-130 6-3 9-3q6-4 7 0l-2 13q-3 21-21 36-6 6-9 3-3-2-5-11l-2-7-2-9q-4-10-3-13 2-3 6-2l3-1h4zm0 0-9 4-4 1h-3q-4 0-5 2l2 12 3 9 2 7q2 9 4 10 3 3 9-2 17-16 21-36l1-12q-1-3-6-1l-9 4zm-4 7 4-1 8-4h6l1 5v1c0 6-5 17-11 23q-7 6-10 2-3 0-7-13-3-7-1-10l6-3h4m-2 2h-2l-4 2q-2 2 1 9l6 12q3 2 7-3c7-5 11-16 11-21v-1l-1-3h-4l-4 2-3 1-5 2zm-228-89-7 12-3 3-2 3-5 7q-4 9-13 14l-16 8c-11 4-16 1-27-16q-4-7-18-13l-14-6q-7-3-7-6 1-3 16-6 22-4 40-3h12l6-3 3-2c10-6 13-8 18-9q9-5 14-2 10 4 3 19m-2 0c4-11 4-14-2-17q-4-2-13 1l-17 10-2 2-7 3h-13a138 138 0 0 0-51 6l-3 2 6 3 14 6q15 6 19 13c10 17 15 20 25 16l15-7q8-6 12-13l5-8 3-4 2-3q6-5 7-10m-7-1q-11 20-33 25-12 3-13-5t6-9q3 1 15-10l4-2q6-6 11-6 8-3 11-2 4 2-1 9m0 0q3-7 1-8-3-1-11 1l-10 6-4 3q-13 11-16 10-5 1-5 7 2 8 12 6 21-6 33-25M461 34c14 4 21 4 32 0l11-7 11-7q6-3 11-1 6 3 7 28c0 14-1 29-3 34q-2 7-11 10l-13 2q-8 0-12 8l-2 3q-3 4-5 4l-4 2-2 1-3 2q-9 5-17 5t-14 5l-5 5q-3 5-10 6l-7 3h-2l-3 1q-8 2-17-4l-6-4-9-1-2-1q-7 0-10-3-8-4-6-12t15-17l6-5 18-15 2-2 8-7q3-4 7-13l1-2q4-10 7-12l8-8q4-5 19 2m0 0q-15-6-19-1l-8 7-6 12-1 2q-4 10-8 13l-9 9a97 97 0 0 1-24 21q-13 9-15 16-1 6 5 12l10 2 2 1q7 0 9 2l6 3q10 6 17 4h3l2-1 6-2 11-6 4-5q6-6 15-7 7 0 17-4l2-1 3-2 4-1q2-1 4-5l2-3q4-8 13-8 6 1 13-2 8-3 11-9l2-34q-1-24-6-28-5-1-11 2l-11 7-11 6c-11 6-18 6-32 0m14 21 10-1 15-7 6-6 5-5q5-5 10 1 8 12 0 27l-3 6-1 3v1l-4 5q-3 3-12 4l-3 1q-9 1-12 6-5 6-15 10l-2 1-2 1-7 4-10 3q-11 2-17 10-7 7-17 6-6 0-23-9-10-5-2-14c3-4 4-4 19-16q12-9 17-15l5-8q9-27 23-16l7 5q4 3 13 3m0 2q-9 0-14-3l-7-5q-13-10-20 14l-5 9q-5 7-17 15l-19 17q-7 7 0 10c13 6 18 9 23 9q9 2 15-6 7-7 19-10 6-1 8-3l8-4 2-1h2q10-5 14-11 4-4 14-6l3-1q8-1 10-4l4-4v-1l2-3 2-5q8-15 0-25-3-6-7-1l-4 4q-5 6-8 7l-15 7zm-13 9q8 5 19 0l7-2q4 0 5 5 3 7-12 13l-20 7q-12 5-19 10c-13 10-18 12-23 10l-4-2-2-1-2-2q-5-3 1-7l13-10q11-8 15-18 4-12 6-14 3-1 7 2l4 4zm0 0-6-4-4-4q-4-3-5-2-3 1-7 13-3 10-14 19l-14 10q-4 4-1 6l3 1 2 1 3 2c5 2 10 0 23-9l19-11 20-7q14-6 12-12-2-4-5-4l-7 2q-11 6-19-1' ), + ), + ), + + 'waves' => array( + 'width' => 120, + 'height' => 40, + 'paths' => array( + array( + 'd' => 'M0 20Q30 5 60 20T120 20', + 'stroke' => true, + 'strokeWidth' => 2, + ), + ), + ), + + 'grain' => array( + 'width' => 4, + 'height' => 4, + 'paths' => array( + array( 'd' => 'M1 3h1v1H1V3zm2-2h1v1H3V1z' ), + ), + ), + + 'ripples' => array( + 'width' => 48, + 'height' => 48, + 'paths' => array( + array( + 'd' => 'M24 17a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM24 9a15 15 0 1 0 0 30 15 15 0 0 0 0-30zM24 1a23 23 0 1 0 0 46 23 23 0 0 0 0-46z', + 'stroke' => true, + 'strokeWidth' => 1.5, + ), + ), + ), + + 'bubbles' => array( + 'width' => 100, + 'height' => 100, + 'paths' => array( + array( 'd' => 'M11,18 C14.8659932,18 18,14.8659932 18,11 C18,7.13400675 14.8659932,4 11,4 C7.13400675,4 4,7.13400675 4,11 C4,14.8659932 7.13400675,18 11,18 Z M59,43 C62.8659932,43 66,39.8659932 66,36 C66,32.1340068 62.8659932,29 59,29 C55.1340068,29 52,32.1340068 52,36 C52,39.8659932 55.1340068,43 59,43 Z M16,36 C17.6568542,36 19,34.6568542 19,33 C19,31.3431458 17.6568542,30 16,30 C14.3431458,30 13,31.3431458 13,33 C13,34.6568542 14.3431458,36 16,36 Z M79,67 C80.6568542,67 82,65.6568542 82,64 C82,62.3431458 80.6568542,61 79,61 C77.3431458,61 76,62.3431458 76,64 C76,65.6568542 77.3431458,67 79,67 Z M34,90 C35.6568542,90 37,88.6568542 37,87 C37,85.3431458 35.6568542,84 34,84 C32.3431458,84 31,85.3431458 31,87 C31,88.6568542 32.3431458,90 34,90 Z M90,14 C91.6568542,14 93,12.6568542 93,11 C93,9.34314575 91.6568542,8 90,8 C88.3431458,8 87,9.34314575 87,11 C87,12.6568542 88.3431458,14 90,14 Z M12,86 C14.209139,86 16,84.209139 16,82 C16,79.790861 14.209139,78 12,78 C9.790861,78 8,79.790861 8,82 C8,84.209139 9.790861,86 12,86 Z M40,21 C42.209139,21 44,19.209139 44,17 C44,14.790861 42.209139,13 40,13 C37.790861,13 36,14.790861 36,17 C36,19.209139 37.790861,21 40,21 Z M63,10 C65.7614237,10 68,7.76142375 68,5 C68,2.23857625 65.7614237,0 63,0 C60.2385763,0 58,2.23857625 58,5 C58,7.76142375 60.2385763,10 63,10 Z M57,70 C59.209139,70 61,68.209139 61,66 C61,63.790861 59.209139,62 57,62 C54.790861,62 53,63.790861 53,66 C53,68.209139 54.790861,70 57,70 Z M86,92 C88.7614237,92 91,89.7614237 91,87 C91,84.2385763 88.7614237,82 86,82 C83.2385763,82 81,84.2385763 81,87 C81,89.7614237 83.2385763,92 86,92 Z M32,63 C34.7614237,63 37,60.7614237 37,58 C37,55.2385763 34.7614237,53 32,53 C29.2385763,53 27,55.2385763 27,58 C27,60.7614237 29.2385763,63 32,63 Z M89,50 C91.7614237,50 94,47.7614237 94,45 C94,42.2385763 91.7614237,40 89,40 C86.2385763,40 84,42.2385763 84,45 C84,47.7614237 86.2385763,50 89,50 Z M80,29 C81.1045695,29 82,28.1045695 82,27 C82,25.8954305 81.1045695,25 80,25 C78.8954305,25 78,25.8954305 78,27 C78,28.1045695 78.8954305,29 80,29 Z M60,91 C61.1045695,91 62,90.1045695 62,89 C62,87.8954305 61.1045695,87 60,87 C58.8954305,87 58,87.8954305 58,89 C58,90.1045695 58.8954305,91 60,91 Z M35,41 C36.1045695,41 37,40.1045695 37,39 C37,37.8954305 36.1045695,37 35,37 C33.8954305,37 33,37.8954305 33,39 C33,40.1045695 33.8954305,41 35,41 Z M12,60 C13.1045695,60 14,59.1045695 14,58 C14,56.8954305 13.1045695,56 12,56 C10.8954305,56 10,56.8954305 10,58 C10,59.1045695 10.8954305,60 12,60 Z' ), + ), + ), + + 'wiggle' => array( + 'width' => 52, + 'height' => 26, + 'paths' => array( + array( + 'd' => 'M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z', + 'fillRule' => 'evenodd', + ), + ), + ), + + // ── TEXTURE ────────────────────────────────────── + + 'crosshatch' => array( + 'width' => 8, + 'height' => 8, + 'paths' => array( + array( 'd' => 'M7 0h1L0 8V7zM8 7v1H7zM0 0h1L8 7v1H7L0 1z' ), + ), + ), + + 'noise' => array( + 'width' => 16, + 'height' => 16, + 'paths' => array( + array( 'd' => 'M2 4h1v1H2zM7 1h1v1H7zM12 6h1v1h-1zM4 10h1v1H4zM9 13h1v1H9zM14 8h1v1h-1zM1 14h1v1H1zM6 11h1v1H6zM11 3h1v1h-1zM3 7h1v1H3zM8 15h1v1H8zM15 2h1v1h-1z' ), + ), + ), + + 'stipple' => array( + 'width' => 60, + 'height' => 60, + 'paths' => array( + array( 'd' => 'M8 10a2 2 0 1 0 4 0 2 2 0 0 0-4 0zM40.5 15a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zM25 45a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM49.5 35a2.5 2.5 0 1 0 5 0 2.5 2.5 0 0 0-5 0zM15 30a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM44.5 55a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0z' ), + ), + ), + + 'linen' => array( + 'width' => 4, + 'height' => 4, + 'paths' => array( + array( 'd' => 'M0 0h2v1H0zM3 2v2h1V2z' ), + ), + ), + + // ── DEPTH ──────────────────────────────────────── + + 'scales' => array( + 'width' => 48, + 'height' => 24, + 'paths' => array( + array( + 'd' => 'M0 0c0 6.63 5.37 12 12 12s12-5.37 12-12M24 0c0 6.63 5.37 12 12 12s12-5.37 12-12M-12 12c0 6.63 5.37 12 12 12s12-5.37 12-12M12 12c0 6.63 5.37 12 12 12s12-5.37 12-12M36 12c0 6.63 5.37 12 12 12s12-5.37 12-12', + 'stroke' => true, + 'strokeWidth' => 1.5, + ), + ), + ), + + // ── TECHNICAL ──────────────────────────────────── + + 'circuit-board' => array( + 'width' => 304, + 'height' => 304, + 'paths' => array( + array( + 'd' => 'M44.1 224a5 5 0 1 1 0 2H0v-2h44.1zm160 48a5 5 0 1 1 0 2H82v-2h122.1zm57.8-46a5 5 0 1 1 0-2H304v2h-42.1zm0 16a5 5 0 1 1 0-2H304v2h-42.1zm6.2-114a5 5 0 1 1 0 2h-86.2a5 5 0 1 1 0-2h86.2zm-256-48a5 5 0 1 1 0 2H0v-2h12.1zm185.8 34a5 5 0 1 1 0-2h86.2a5 5 0 1 1 0 2h-86.2zM258 12.1a5 5 0 1 1-2 0V0h2v12.1zm-64 208a5 5 0 1 1-2 0v-54.2a5 5 0 1 1 2 0v54.2zm48-198.2V80h62v2h-64V21.9a5 5 0 1 1 2 0zm16 16V64h46v2h-48V37.9a5 5 0 1 1 2 0zm-128 96V208h16v12.1a5 5 0 1 1-2 0V210h-16v-76.1a5 5 0 1 1 2 0zm-5.9-21.9a5 5 0 1 1 0 2H114v48H85.9a5 5 0 1 1 0-2H112v-48h12.1zm-6.2 130a5 5 0 1 1 0-2H176v-74.1a5 5 0 1 1 2 0V242h-60.1zm-16-64a5 5 0 1 1 0-2H114v48h10.1a5 5 0 1 1 0 2H112v-48h-10.1zM66 284.1a5 5 0 1 1-2 0V274H50v30h-2v-32h18v12.1zM236.1 176a5 5 0 1 1 0 2H226v94h48v32h-2v-30h-48v-98h12.1zm25.8-30a5 5 0 1 1 0-2H274v44.1a5 5 0 1 1-2 0V146h-10.1zm-64 96a5 5 0 1 1 0-2H208v-80h16v-14h-42.1a5 5 0 1 1 0-2H226v18h-16v80h-12.1zm86.2-210a5 5 0 1 1 0 2H272V0h2v32h10.1zM98 101.9V146H53.9a5 5 0 1 1 0-2H96v-42.1a5 5 0 1 1 2 0zM53.9 34a5 5 0 1 1 0-2H80V0h2v34H53.9zm60.1 3.9V66H82v64H69.9a5 5 0 1 1 0-2H80V64h32V37.9a5 5 0 1 1 2 0zM101.9 82a5 5 0 1 1 0-2H128V37.9a5 5 0 1 1 2 0V82h-28.1zm16-64a5 5 0 1 1 0-2H146v44.1a5 5 0 1 1-2 0V18h-26.1zm102.2 270a5 5 0 1 1 0 2H98v14h-2v-16h124.1zM242 149.9V160h16v34h-16v62h48v48h-2v-46h-48v-66h16v-30h-16v-12.1a5 5 0 1 1 2 0zM53.9 18a5 5 0 1 1 0-2H64V2H48V0h18v18H53.9zm112 32a5 5 0 1 1 0-2H192V0h50v2h-48v48h-28.1zm-48-48a5 5 0 0 1-9.8-2h2.07a3 3 0 1 0 5.66 0H178v34h-18V21.9a5 5 0 1 1 2 0V32h14V2h-58.1zm0 96a5 5 0 1 1 0-2H137l32-32h39V21.9a5 5 0 1 1 2 0V66h-40.17l-32 32H117.9zm28.1 90.1a5 5 0 1 1-2 0v-76.51L175.59 80H224V21.9a5 5 0 1 1 2 0V82h-49.59L146 112.41v75.69zm16 32a5 5 0 1 1-2 0v-99.51L184.59 96H300.1a5 5 0 0 1 .1-2h-117.59l-32 32v99.51zM0 157.9V146h44.1a5 5 0 1 1 0 2H2v8H0v1.9zm0 16V162h46v46h-16v4a5 5 0 0 1-2 0v-6h18v-46H0zm256 32V222h-34.1a5 5 0 1 1 0-2H254v-28h2v14zm40 32v38h-2v-36h-48v36h-2v-38h52zM258 284.1a5 5 0 1 1-2 0V274h-44v-2h46v12.1zM130 228.1a5 5 0 1 1-2 0V210h-16v-24h-4v-2h6v26h16v18.1zM194 66V46h-18V0h2v44h18v24h62v2h-64zm0-32V2h62v2h-60v30h-2zm10 44a5 5 0 1 1 0-2h22V46h46v2h-44v28h-24zm-20-2h-2v-30h34v2h-32v28zM0 197.9V190h18v2H2v4h-2v1.9zm0 12V202h2v6h14v2H0v-.1zm0-12V200h2v-2H0zm52-56V130h-2v12h-48v2h50zm-2 18V162H0v2h50v-2zM0 195.9V190h2v4h-2v1.9zm0 0V196H2v-2H0v1.9z', + 'fillRule' => 'evenodd', + ), + ), + ), + + 'graph-paper' => array( + 'width' => 100, + 'height' => 100, + 'paths' => array( + array( + 'd' => 'M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z', + 'opacity' => 0.5, + ), + array( + 'd' => 'M6 5V0H5v5H0v1h5v94h1V6h94V5H6z', + ), + ), + ), + ); +} diff --git a/functions/blocks/block-support-routing.php b/functions/blocks/block-support-routing.php new file mode 100644 index 0000000..9340f69 --- /dev/null +++ b/functions/blocks/block-support-routing.php @@ -0,0 +1,237 @@ + '', + 'wrapper' => '', + 'inner_raw' => array(), + ); + } + + $inner = array(); + $wrapper = $style; + + foreach ( $inner_paths as $path ) { + $keys = explode( '.', $path ); + + if ( 1 === count( $keys ) ) { + $key = $keys[0]; + if ( isset( $wrapper[ $key ] ) ) { + $inner[ $key ] = $wrapper[ $key ]; + unset( $wrapper[ $key ] ); + } + continue; + } + + list( $group, $key ) = $keys; + if ( isset( $wrapper[ $group ][ $key ] ) ) { + $inner[ $group ][ $key ] = $wrapper[ $group ][ $key ]; + unset( $wrapper[ $group ][ $key ] ); + + if ( empty( $wrapper[ $group ] ) ) { + unset( $wrapper[ $group ] ); + } + } + } + + $inner_styles = wp_style_engine_get_styles( $inner ); + $wrapper_styles = wp_style_engine_get_styles( $wrapper ); + + return array( + 'inner' => isset( $inner_styles['css'] ) ? $inner_styles['css'] : '', + 'wrapper' => isset( $wrapper_styles['css'] ) ? $wrapper_styles['css'] : '', + 'inner_raw' => $inner, + ); + } +} + +if ( ! function_exists( 'airowp_visual_support_classes' ) ) { + /** + * The `has-*` classes core generates for a block's relocated style groups. + * + * WordPress stores a PRESET pick (a theme palette/font entry) in top-level + * block attributes — `backgroundColor`, `textColor`, `gradient`, + * `borderColor`, `fontSize`, `fontFamily` — and a CUSTOM value in the + * `style` attribute. Only presets produce a `has-*` class (see + * WP_Style_Engine::BLOCK_STYLE_DEFINITIONS_METADATA's `classnames` entries), + * so those classes are derived from attributes here rather than sniffed out + * of a rendered class string. + * + * A preset can also arrive *inside* `style` as a `var:preset|…` value (some + * blocks write color picks straight into `style.color.background` instead of + * the native `backgroundColor` attribute). The Style Engine still turns that + * into the matching class, so `$inner_style_group` — the already-sliced + * inner portion of `style` from airowp_split_style_groups() — is run + * back through it with `convert_vars_to_classnames` to catch that case too. + * + * Only classes belonging to a style group actually present in + * `$inner_paths` are returned — e.g. requesting `color` alone must not pull + * a `has-*-font-size` class off the wrapper. + * + * @param array $attributes Block attributes. + * @param array $inner_paths Dot paths being relocated, e.g. array( 'color', 'border' ). + * @param array $inner_style_group The `inner_raw` value from airowp_split_style_groups(). + * @return string[] Classes to relocate from the wrapper to the inner element. + */ + function airowp_visual_support_classes( $attributes, $inner_paths, $inner_style_group = array() ) { + $groups = array(); + foreach ( $inner_paths as $path ) { + $segments = explode( '.', $path ); + $groups[] = $segments[0]; + } + $groups = array_unique( $groups ); + + $classes = array(); + + if ( in_array( 'color', $groups, true ) ) { + $background_color = isset( $attributes['backgroundColor'] ) ? (string) $attributes['backgroundColor'] : ''; + $text_color = isset( $attributes['textColor'] ) ? (string) $attributes['textColor'] : ''; + $gradient = isset( $attributes['gradient'] ) ? (string) $attributes['gradient'] : ''; + + if ( '' !== $background_color ) { + $classes[] = "has-{$background_color}-background-color"; + } + if ( '' !== $gradient ) { + $classes[] = "has-{$gradient}-gradient-background"; + } + if ( '' !== $background_color || '' !== $gradient ) { + $classes[] = 'has-background'; + } + if ( '' !== $text_color ) { + $classes[] = "has-{$text_color}-color"; + $classes[] = 'has-text-color'; + } + if ( isset( $attributes['style']['elements']['link']['color'] ) ) { + $classes[] = 'has-link-color'; + } + } + + if ( in_array( 'border', $groups, true ) ) { + $border_color = isset( $attributes['borderColor'] ) ? (string) $attributes['borderColor'] : ''; + + if ( '' !== $border_color ) { + $classes[] = "has-{$border_color}-border-color"; + $classes[] = 'has-border-color'; + } + } + + if ( in_array( 'typography', $groups, true ) ) { + $font_size = isset( $attributes['fontSize'] ) ? (string) $attributes['fontSize'] : ''; + $font_family = isset( $attributes['fontFamily'] ) ? (string) $attributes['fontFamily'] : ''; + + if ( '' !== $font_size ) { + $classes[] = "has-{$font_size}-font-size"; + } + if ( '' !== $font_family ) { + $classes[] = "has-{$font_family}-font-family"; + } + } + + if ( ! empty( $inner_style_group ) ) { + $engine_styles = wp_style_engine_get_styles( $inner_style_group, array( 'convert_vars_to_classnames' => true ) ); + if ( ! empty( $engine_styles['classnames'] ) ) { + $classes = array_merge( $classes, explode( ' ', $engine_styles['classnames'] ) ); + } + } + + return array_values( array_unique( $classes ) ); + } +} + +if ( ! function_exists( 'airowp_route_visual_supports' ) ) { + /** + * Move visual styles and classes from a block wrapper onto an inner element. + * + * @param string $html Rendered block HTML; the outer tag is the wrapper. + * @param array $attributes Block attributes (reads `style` plus the preset attributes + * for each requested style group, e.g. `backgroundColor`). + * @param string $inner_class Class identifying the inner visual element. + * @param array $inner_paths Dot paths to move, e.g. array( 'color', 'border' ). + * @return string Updated HTML. + */ + function airowp_route_visual_supports( $html, $attributes, $inner_class, $inner_paths ) { + $style = ( isset( $attributes['style'] ) && is_array( $attributes['style'] ) ) + ? $attributes['style'] + : array(); + + $split = airowp_split_style_groups( $style, $inner_paths ); + $classes_to_move = airowp_visual_support_classes( $attributes, $inner_paths, $split['inner_raw'] ); + + $processor = new WP_HTML_Tag_Processor( $html ); + if ( ! $processor->next_tag() ) { + return $html; + } + + if ( '' !== $split['wrapper'] ) { + $processor->set_attribute( 'style', $split['wrapper'] ); + } else { + $processor->remove_attribute( 'style' ); + } + + foreach ( $classes_to_move as $class ) { + $processor->remove_class( $class ); + } + + $has_inner_work = '' !== $split['inner'] || ! empty( $classes_to_move ); + if ( ! $has_inner_work ) { + return $processor->get_updated_html(); + } + + while ( $processor->next_tag() ) { + if ( ! $processor->has_class( $inner_class ) ) { + continue; + } + + foreach ( $classes_to_move as $class ) { + $processor->add_class( $class ); + } + + if ( '' !== $split['inner'] ) { + $existing = (string) $processor->get_attribute( 'style' ); + $existing = ( '' !== $existing && ';' !== substr( $existing, -1 ) ) + ? $existing . ';' + : $existing; + $processor->set_attribute( 'style', $existing . $split['inner'] ); + } + + break; + } + + return $processor->get_updated_html(); + } +} diff --git a/functions/blocks/helpers.php b/functions/blocks/helpers.php new file mode 100644 index 0000000..ff1e68c --- /dev/null +++ b/functions/blocks/helpers.php @@ -0,0 +1,321 @@ + 0x10FFFF + || ( $codepoint >= 0xD800 && $codepoint <= 0xDFFF ) + ) { + return ''; + } + + return (string) mb_chr( $codepoint, 'UTF-8' ); + }, + (string) $css + ); + + // Any remaining backslash escape of a printable ASCII char, e.g. `\u rl(`. + $css = preg_replace( '/\\\\([\x20-\x7E])/', '$1', (string) $css ); + + return (string) $css; +} + +/** + * Sanitize CSS value with strict validation. + * + * Prevents CSS injection attacks by validating against allowed patterns. + * Blocks dangerous CSS functions like expression(), url('javascript:...'), etc. + * + * @param mixed $value Value to sanitize. + * @param string $type Expected type: 'size', 'color', 'url'. Default 'size'. + * @return string|null Sanitized value or null if invalid. + */ +function airowp_sanitize_css_value( $value, $type = 'size' ) { + if ( empty( $value ) && '0' !== $value ) { + return null; + } + + switch ( $type ) { + case 'size': + case 'spacing': + case 'dimension': + return airowp_sanitize_css_size( $value ); + + case 'color': + return airowp_sanitize_css_color( $value ); + + case 'url': + return esc_url( $value ); + + default: + return sanitize_text_field( $value ); + } +} + +/** + * Sanitize CSS size/spacing value. + * + * Allows: px, em, rem, %, vh, vw, vmin, vmax + * Allows: calc(), clamp(), min(), max() with safe content + * Blocks: expression(), url(), var() with user input + * + * @param string $value Size value to sanitize. + * @return string|null Sanitized size or null if invalid. + */ +function airowp_sanitize_css_size( $value ) { + $value = trim( $value ); + + // Allow CSS keywords. + $keywords = array( 'auto', 'inherit', 'initial', 'unset', 'none', '0' ); + if ( in_array( strtolower( $value ), $keywords, true ) ) { + return strtolower( $value ); + } + + // Allow simple numeric values with units (e.g., "24px", "1.5rem", "-10px"). + if ( preg_match( '/^-?\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax)$/i', $value ) ) { + return $value; + } + + // Allow CSS math functions with safe values only. + // Examples: calc(100% - 20px), clamp(1rem, 2vw, 3rem), min(50%, 300px), max(100px, 10vw). + if ( preg_match( '/^(calc|clamp|min|max)\s*\([\d\s\.,\+\-\*\/\(\)%a-z]+\)$/i', $value ) ) { + // Additional validation: no dangerous functions inside, and parens must be balanced. + if ( ! preg_match( '/(expression|url|attr|var)/i', $value ) && substr_count( $value, '(' ) === substr_count( $value, ')' ) ) { + return $value; + } + } + + wp_trigger_error( __FUNCTION__, sprintf( 'airo-wp: Invalid CSS size value rejected: %s', $value ), E_USER_NOTICE ); + + return null; +} + +/** + * Sanitize CSS color value. + * + * Allows: hex (#fff, #ffffff), rgb(), rgba(), hsl(), hsla(), named colors, CSS custom properties + * Blocks: url(), expression(), inline scripts + * + * @param string $value Color value to sanitize. + * @return string|null Sanitized color or null if invalid. + */ +function airowp_sanitize_css_color( $value ) { + $value = trim( $value ); + + // Allow hex colors (#fff, #ffffff, #ffffffff). + if ( preg_match( '/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $value ) ) { + return strtolower( $value ); + } + + // Allow rgb/rgba (e.g., "rgb(255, 0, 0)", "rgba(255, 0, 0, 0.5)"). + if ( preg_match( '/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d\.]+\s*)?\)$/i', $value ) ) { + return $value; + } + + // Allow hsl/hsla (e.g., "hsl(120, 100%, 50%)", "hsla(120, 100%, 50%, 0.5)"). + if ( preg_match( '/^hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[\d\.]+\s*)?\)$/i', $value ) ) { + return $value; + } + + // Allow CSS custom properties for theme integration (e.g., "var(--wp--preset--color--primary)"). + // Only allow WordPress-style custom properties (--wp--*, --airo-wp--*). + if ( preg_match( '/^var\(\s*--(wp|dsgo|dsg)--[\w\-]+\s*\)$/i', $value ) ) { + return $value; + } + + // Allow standard CSS named colors. + $named_colors = array( + 'transparent', + 'currentcolor', + 'black', + 'white', + 'red', + 'green', + 'blue', + 'yellow', + 'orange', + 'purple', + 'pink', + 'brown', + 'gray', + 'grey', + 'silver', + 'navy', + 'teal', + 'aqua', + 'lime', + 'maroon', + 'olive', + 'fuchsia', + ); + + if ( in_array( strtolower( $value ), $named_colors, true ) ) { + return strtolower( $value ); + } + + wp_trigger_error( __FUNCTION__, sprintf( 'airo-wp: Invalid CSS color value rejected: %s', $value ), E_USER_NOTICE ); + + return null; +} + +/** + * Check if block is airo-wp block. + * + * @param string $block_name Block name. + * @return bool + */ +function airowp_is_dsg_block( $block_name ) { + return strpos( $block_name, 'airo-wp/' ) === 0; +} + +/** + * Resolve a WordPress preset color reference to its concrete value. + * + * Color controls store theme palette selections as the WordPress preset + * shorthand "var:preset|color|{slug}" (or the legacy CSS-variable form + * "var(--wp--preset--color--{slug})") so the block stays in sync with theme + * color changes. Some consumers — SVG data URIs, JavaScript map markers, or + * any context that cannot inherit the page's CSS custom properties — need the + * actual color value instead. This looks up the slug in the global settings + * palette and returns the resolved color, leaving raw values untouched. + * + * When the value is a preset reference but the slug is not present in the + * current palette — e.g. content authored under a theme whose palette a later + * theme dropped — the token cannot be used as a CSS color. Pass $fallback to + * substitute a safe default in that case; when omitted, the original token is + * returned unchanged (matching WordPress' own pass-through behavior). + * + * @param mixed $color Color value, possibly a preset reference. + * Non-string / empty input is returned as-is. + * @param string|null $fallback Value returned when the color is a preset + * reference whose slug is not found in the + * palette. Default null (return the original). + * @return mixed Resolved color string, the $fallback for an unresolvable + * preset, or the original value (which may be null / non-string) + * when it is not a preset reference. + */ +function airowp_resolve_preset_color( $color, $fallback = null ) { + if ( ! is_string( $color ) || '' === $color ) { + return $color; + } + + // Accept both the block-attribute shorthand var:preset|color|{slug} and the + // CSS-var form var(--wp--preset--color--{slug}). + if ( ! preg_match( '/^var:preset\|color\|(.+)$/', $color, $matches ) + && ! preg_match( '/^var\(--wp--preset--color--(.+)\)$/', $color, $matches ) ) { + return $color; + } + + $slug = $matches[1]; + $palette = function_exists( 'wp_get_global_settings' ) + ? wp_get_global_settings( array( 'color', 'palette' ) ) + : null; + + if ( is_array( $palette ) ) { + // Palette is grouped by origin (theme, default, custom). + foreach ( $palette as $origin_colors ) { + if ( ! is_array( $origin_colors ) ) { + continue; + } + foreach ( $origin_colors as $entry ) { + if ( isset( $entry['slug'], $entry['color'] ) && is_string( $entry['color'] ) && $entry['slug'] === $slug ) { + return $entry['color']; + } + } + } + } + + // Preset reference that could not be resolved to a palette color. + return null === $fallback ? $color : $fallback; +} diff --git a/functions/blocks/index.php b/functions/blocks/index.php new file mode 100644 index 0000000..19a8343 --- /dev/null +++ b/functions/blocks/index.php @@ -0,0 +1,13 @@ +name ); + return $url ? (string) $url : null; + } + if ( $object instanceof \WP_User ) { + return (string) get_author_posts_url( $object->ID ); + } + return null; + } + ); + } + + /** + * Whether the current request is an archive of any kind. + * + * @return bool + */ + private static function is_archive_context() { + return is_archive() || is_home() || is_search(); + } + + /** + * Registers one source with both core Bindings and our metadata registry. + * + * @param Registry $registry Metadata registry. + * @param string $slug Binding source slug. + * @param string $label Display label. + * @param string[] $returns Return types. + * @param callable $callback Value callback. + */ + private static function register_one( Registry $registry, $slug, $label, array $returns, callable $callback ) { + airowp_register_bindings_source( + $slug, + $callback, + array( 'label' => $label ) + ); + $registry->register_source( + $slug, + array( + 'label' => $label, + 'group' => 'archive', + 'returns' => $returns, + ) + ); + } +} diff --git a/includes/Blocks/Common/DynamicTags/Bootstrap.php b/includes/Blocks/Common/DynamicTags/Bootstrap.php new file mode 100644 index 0000000..bd79647 --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/Bootstrap.php @@ -0,0 +1,161 @@ +rest = new RestController( Registry::instance() ); + $this->rest->register_hooks(); + } + + /** + * Returns the REST controller instance (test/debug accessor). + * + * @return RestController|null + */ + public function get_rest_controller() { + return $this->rest; + } + + /** + * Registers the four source families and the custom-field metadata. + */ + public function register_sources() { + $registry = Registry::instance(); + + PostSources::register( $registry ); + SiteSources::register( $registry ); + ArchiveSources::register( $registry ); + UserSources::register( $registry ); + + $this->register_custom_field_metadata( $registry ); + + /** + * Fires once all built-in Dynamic Tag sources are registered. + * + * Third-party plugins should hook here to register additional + * sources via `$registry->register_source()` and (for bindings + * value resolution) `airowp_register_bindings_source()`. + * + * @since 2.2.0 + * + * @param Registry $registry Metadata registry. + */ + do_action( 'airowp_dynamic_tags_registered', $registry ); + } + + /** + * Adds picker-side metadata for the pre-existing custom-field bindings + * (post-meta, ACF, Meta Box, Pods, JetEngine) that were registered by + * the Query Bindings subsystem. Value callbacks are already registered + * there — this just gives the picker discovery + label info. + * + * @param Registry $registry Metadata registry. + */ + private function register_custom_field_metadata( Registry $registry ) { + $image_subkey_arg = array( + 'subkey' => array( + 'type' => 'string', + 'enum' => array( 'url', 'id', 'alt', 'width', 'height', 'title', 'caption' ), + ), + ); + $key_arg = array( + 'key' => array( + 'type' => 'string', + 'required' => true, + ), + ); + + $registry->register_source( + 'airo-wp/post-meta', + array( + 'label' => __( 'Post meta', 'airo-wp' ), + 'group' => 'custom-fields', + 'returns' => array( 'text', 'url', 'number', 'date' ), + 'args' => array_merge( $key_arg, $image_subkey_arg ), + 'discovery_cb' => array( FieldDiscovery::class, 'post_meta' ), + ) + ); + + if ( function_exists( 'get_field' ) ) { + $registry->register_source( + 'airo-wp/acf', + array( + 'label' => __( 'ACF field', 'airo-wp' ), + 'group' => 'custom-fields', + 'returns' => array( 'text', 'image', 'url', 'number', 'date' ), + 'args' => array_merge( $key_arg, $image_subkey_arg ), + 'discovery_cb' => array( FieldDiscovery::class, 'acf' ), + ) + ); + } + + if ( function_exists( 'rwmb_meta' ) ) { + $registry->register_source( + 'airo-wp/metabox', + array( + 'label' => __( 'Meta Box field', 'airo-wp' ), + 'group' => 'custom-fields', + 'returns' => array( 'text', 'image', 'url', 'number', 'date' ), + 'args' => array_merge( $key_arg, $image_subkey_arg ), + ) + ); + } + + if ( function_exists( 'pods_field' ) ) { + $registry->register_source( + 'airo-wp/pods', + array( + 'label' => __( 'Pods field', 'airo-wp' ), + 'group' => 'custom-fields', + 'returns' => array( 'text', 'image', 'url', 'number', 'date' ), + 'args' => array_merge( $key_arg, $image_subkey_arg ), + ) + ); + } + + if ( class_exists( 'Jet_Engine' ) && function_exists( 'jet_engine' ) ) { + $registry->register_source( + 'airo-wp/jetengine', + array( + 'label' => __( 'JetEngine field', 'airo-wp' ), + 'group' => 'custom-fields', + 'returns' => array( 'text', 'image', 'url', 'number', 'date' ), + 'args' => array_merge( $key_arg, $image_subkey_arg ), + ) + ); + } + } +} diff --git a/includes/Blocks/Common/DynamicTags/FieldDiscovery.php b/includes/Blocks/Common/DynamicTags/FieldDiscovery.php new file mode 100644 index 0000000..d63255c --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/FieldDiscovery.php @@ -0,0 +1,145 @@ +> + */ + public static function acf( array $context ) { + if ( ! function_exists( 'acf_get_field_groups' ) || ! function_exists( 'acf_get_fields' ) ) { + return array(); + } + $post_type = isset( $context['post_type'] ) ? (string) $context['post_type'] : 'post'; + $returns = isset( $context['returns'] ) ? (string) $context['returns'] : ''; + + $groups = \acf_get_field_groups( array( 'post_type' => $post_type ) ); + $fields = array(); + foreach ( $groups as $group ) { + $group_fields = \acf_get_fields( $group ); + if ( ! is_array( $group_fields ) ) { + continue; + } + foreach ( $group_fields as $field ) { + $returns_type = self::acf_type_to_returns( $field['type'] ?? '' ); + if ( null === $returns_type ) { + continue; + } + if ( '' !== $returns && $returns !== $returns_type ) { + continue; + } + $fields[] = array( + 'key' => (string) ( $field['name'] ?? '' ), + 'label' => (string) ( $field['label'] ?? $field['name'] ?? '' ), + 'returns' => $returns_type, + 'group' => (string) ( $group['title'] ?? '' ), + ); + } + } + return $fields; + } + + /** + * Field discovery for `airo-wp/post-meta`. + * + * Uses register_meta()-declared keys (public ones only, `show_in_rest`) + * because the raw postmeta table can contain thousands of internal keys + * across plugins. This matches WP core's own picker behavior. + * + * @param array $context { post_type, returns }. + * @return array> + */ + public static function post_meta( array $context ) { + $post_type = isset( $context['post_type'] ) ? (string) $context['post_type'] : 'post'; + + $meta = get_registered_meta_keys( 'post', $post_type ); + $items = array(); + foreach ( $meta as $key => $meta_data ) { + if ( 0 === strpos( (string) $key, '_' ) ) { + continue; + } + if ( empty( $meta_data['show_in_rest'] ) ) { + continue; + } + $items[] = array( + 'key' => (string) $key, + 'label' => (string) ( $meta_data['label'] ?? $key ), + 'returns' => 'text', + 'group' => __( 'Registered meta', 'airo-wp' ), + ); + } + return $items; + } + + /** + * Maps ACF field types to our return-type vocabulary. + * + * @param string $acf_type ACF field type name. + * @return string|null Return type, or null when the field is skipped. + */ + private static function acf_type_to_returns( $acf_type ) { + switch ( $acf_type ) { + case 'text': + case 'textarea': + case 'email': + case 'wysiwyg': + case 'select': + case 'radio': + case 'button_group': + case 'color_picker': + return 'text'; + case 'number': + case 'range': + return 'number'; + case 'url': + case 'link': + case 'page_link': + return 'url'; + case 'image': + case 'file': + return 'image'; + case 'date_picker': + case 'time_picker': + case 'date_time_picker': + return 'date'; + // Private / container / repeater / flexible types — skip so + // sensitive values (passwords) and non-scalar shapes never + // surface in the Dynamic Tag picker. + case 'password': + case 'group': + case 'repeater': + case 'flexible_content': + case 'relationship': + case 'post_object': + case 'taxonomy': + case 'user': + case 'clone': + return null; + } + return 'text'; + } +} diff --git a/includes/Blocks/Common/DynamicTags/ImageResolver.php b/includes/Blocks/Common/DynamicTags/ImageResolver.php new file mode 100644 index 0000000..6b551d0 --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/ImageResolver.php @@ -0,0 +1,195 @@ + 0, + 'url' => $url, + 'alt' => '', + 'width' => 0, + 'height' => 0, + ); + } + + return null; + } + + /** + * Attempts to discover an attachment ID for image-typed sources. + * + * @param string $slug Source slug. + * @param array $args Source args. + * @param int $post_id Current post ID context. + * @return int Attachment ID or 0. + */ + private static function resolve_attachment_id( $slug, array $args, $post_id ) { + switch ( $slug ) { + case 'airo-wp/post-featured-image': + return (int) get_post_thumbnail_id( $post_id ); + + case 'airo-wp/site-logo': + $logo = (int) get_option( 'site_logo' ); + if ( ! $logo ) { + $logo = (int) get_theme_mod( 'custom_logo' ); + } + return $logo; + + case 'airo-wp/post-meta': + $key = isset( $args['key'] ) ? (string) $args['key'] : ''; + $value = $key && $post_id ? get_post_meta( $post_id, $key, true ) : ''; + return is_numeric( $value ) ? (int) $value : 0; + + case 'airo-wp/acf': + $key = isset( $args['key'] ) ? (string) $args['key'] : ''; + if ( ! $key || ! $post_id || ! function_exists( 'get_field' ) ) { + return 0; + } + $value = \get_field( $key, $post_id ); + if ( is_array( $value ) && isset( $value['ID'] ) ) { + return (int) $value['ID']; + } + if ( is_numeric( $value ) ) { + return (int) $value; + } + return 0; + } + + return 0; + } + + /** + * Resolves a scalar sub-value (url/id/alt/width/height/title/caption) + * from an attachment ID. Used by image-typed Bindings sources that + * need to project to a single scalar per the Block Bindings contract. + * + * @param int $attachment_id Attachment post ID. + * @param string $subkey Sub-key: url|id|alt|width|height|title|caption. + * @param string $size Image size for url/width/height (default 'full'). + * @return string|null + */ + public static function resolve_subvalue( $attachment_id, $subkey, $size = 'full' ) { + $attachment_id = (int) $attachment_id; + if ( ! $attachment_id ) { + return null; + } + if ( ! in_array( $subkey, array( 'url', 'id', 'alt', 'width', 'height', 'title', 'caption' ), true ) ) { + $subkey = 'url'; + } + + switch ( $subkey ) { + case 'id': + return (string) $attachment_id; + case 'alt': + $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); + return '' === $alt ? null : (string) $alt; + case 'title': + $title = get_the_title( $attachment_id ); + return '' === $title ? null : (string) $title; + case 'caption': + $caption = wp_get_attachment_caption( $attachment_id ); + return '' === $caption || false === $caption ? null : (string) $caption; + case 'width': + case 'height': + $src = wp_get_attachment_image_src( $attachment_id, $size ); + if ( ! $src ) { + return null; + } + $idx = 'width' === $subkey ? 1 : 2; + return (string) $src[ $idx ]; + case 'url': + default: + $src = wp_get_attachment_image_src( $attachment_id, $size ); + return $src && ! empty( $src[0] ) ? (string) $src[0] : null; + } + } + + /** + * Builds an image descriptor from an attachment ID. + * + * @param int $attachment_id Attachment post ID. + * @param string $size Image size. + * @return array{id:int,url:string,alt:string,width:int,height:int}|null + */ + public static function descriptor_from_id( $attachment_id, $size = 'full' ) { + $attachment_id = (int) $attachment_id; + if ( ! $attachment_id ) { + return null; + } + $src = wp_get_attachment_image_src( $attachment_id, $size ); + if ( ! $src || empty( $src[0] ) ) { + return null; + } + $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); + return array( + 'id' => $attachment_id, + 'url' => (string) $src[0], + 'alt' => is_string( $alt ) ? $alt : '', + 'width' => (int) $src[1], + 'height' => (int) $src[2], + ); + } + + /** + * Falls back to the core Bindings API for a URL-only scalar. + * + * @param string $slug Source slug. + * @param array $args Source args. + * @param int $post_id Current post ID. + * @return string Empty string when unresolvable. + */ + private static function resolve_url_via_bindings( $slug, array $args, $post_id ) { + if ( ! function_exists( 'get_block_bindings_source' ) ) { + return ''; + } + $binding = get_block_bindings_source( $slug ); + if ( ! $binding || ! isset( $binding->get_value_callback ) || ! is_callable( $binding->get_value_callback ) ) { + return ''; + } + $args['__airowp_post_id'] = $post_id; + $args['subkey'] = $args['subkey'] ?? 'url'; + $value = call_user_func( $binding->get_value_callback, $args, null, 'url' ); + if ( is_string( $value ) && '' !== $value && filter_var( $value, FILTER_VALIDATE_URL ) ) { + return $value; + } + return ''; + } +} diff --git a/includes/Blocks/Common/DynamicTags/PostSources.php b/includes/Blocks/Common/DynamicTags/PostSources.php new file mode 100644 index 0000000..984b8ed --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/PostSources.php @@ -0,0 +1,301 @@ +post_content ), 40, '…' ); + return '' === $excerpt ? null : (string) $excerpt; + } + ); + + self::register_one( + $registry, + 'airo-wp/post-date', + __( 'Post publish date', 'airo-wp' ), + array( 'text', 'date' ), + static function ( $args, $_block, $attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $format = isset( $args['format'] ) ? (string) $args['format'] : ''; + if ( 'datetime' === $attr || '' === $format ) { + return (string) get_the_date( 'c', $post_id ); + } + return (string) get_the_date( $format, $post_id ); + }, + array( + 'format' => array( + 'type' => 'string', + 'description' => __( 'PHP date format; defaults to the site setting.', 'airo-wp' ), + ), + ) + ); + + self::register_one( + $registry, + 'airo-wp/post-modified-date', + __( 'Post modified date', 'airo-wp' ), + array( 'text', 'date' ), + static function ( $args, $_block, $attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $format = isset( $args['format'] ) ? (string) $args['format'] : ''; + if ( 'datetime' === $attr || '' === $format ) { + return (string) get_the_modified_date( 'c', $post_id ); + } + return (string) get_the_modified_date( $format, $post_id ); + }, + array( + 'format' => array( 'type' => 'string' ), + ) + ); + + self::register_one( + $registry, + 'airo-wp/post-permalink', + __( 'Post permalink', 'airo-wp' ), + array( 'url' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $url = get_permalink( $post_id ); + return $url ? (string) $url : null; + } + ); + + self::register_one( + $registry, + 'airo-wp/post-id', + __( 'Post ID', 'airo-wp' ), + array( 'text', 'number' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + return $post_id ? (string) $post_id : null; + } + ); + + self::register_one( + $registry, + 'airo-wp/post-type', + __( 'Post type', 'airo-wp' ), + array( 'text' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $type = get_post_type( $post_id ); + return $type ? (string) $type : null; + } + ); + + self::register_one( + $registry, + 'airo-wp/post-author-name', + __( 'Post author name', 'airo-wp' ), + array( 'text' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $author_id = (int) get_post_field( 'post_author', $post_id ); + if ( ! $author_id ) { + return null; + } + $name = get_the_author_meta( 'display_name', $author_id ); + return '' === $name ? null : (string) $name; + } + ); + + self::register_one( + $registry, + 'airo-wp/post-author-url', + __( 'Post author archive URL', 'airo-wp' ), + array( 'url' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $author_id = (int) get_post_field( 'post_author', $post_id ); + if ( ! $author_id ) { + return null; + } + $url = get_author_posts_url( $author_id ); + return $url ? (string) $url : null; + } + ); + + self::register_one( + $registry, + 'airo-wp/post-author-avatar-url', + __( 'Post author avatar', 'airo-wp' ), + array( 'image', 'url' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $author_id = (int) get_post_field( 'post_author', $post_id ); + if ( ! $author_id ) { + return null; + } + $size = isset( $args['size'] ) ? (int) $args['size'] : 96; + $url = get_avatar_url( $author_id, array( 'size' => max( 24, $size ) ) ); + return $url ? (string) $url : null; + }, + array( + 'size' => array( 'type' => 'integer' ), + ) + ); + + self::register_one( + $registry, + 'airo-wp/post-featured-image', + __( 'Post featured image', 'airo-wp' ), + array( 'image' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + if ( ! $post_id ) { + return null; + } + $attachment_id = (int) get_post_thumbnail_id( $post_id ); + if ( ! $attachment_id ) { + return null; + } + $subkey = isset( $args['subkey'] ) ? (string) $args['subkey'] : 'url'; + $size = isset( $args['size'] ) ? (string) $args['size'] : 'full'; + return ImageResolver::resolve_subvalue( $attachment_id, $subkey, $size ); + }, + array( + 'subkey' => array( + 'type' => 'string', + 'enum' => array( 'url', 'id', 'alt', 'width', 'height', 'title', 'caption' ), + ), + 'size' => array( 'type' => 'string' ), + ) + ); + + self::register_one( + $registry, + 'airo-wp/post-taxonomy', + __( 'Post taxonomy terms', 'airo-wp' ), + array( 'text' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $post_id = (int) ( $args['__airowp_post_id'] ?? 0 ); + $taxonomy = isset( $args['taxonomy'] ) ? sanitize_key( (string) $args['taxonomy'] ) : 'category'; + if ( ! $post_id || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) { + return null; + } + $separator = isset( $args['separator'] ) ? sanitize_text_field( (string) $args['separator'] ) : ', '; + $terms = get_the_term_list( $post_id, $taxonomy, '', $separator ); + if ( is_wp_error( $terms ) || ! $terms ) { + return null; + } + return wp_strip_all_tags( (string) $terms ); + }, + array( + 'taxonomy' => array( + 'type' => 'string', + 'required' => true, + ), + 'separator' => array( 'type' => 'string' ), + ) + ); + } + + /** + * Shared helper that registers one source with both core Bindings and + * our metadata registry, using the identical slug and default group. + * + * @param Registry $registry Metadata registry. + * @param string $slug Binding source slug. + * @param string $label Display label. + * @param string[] $returns Return types. + * @param callable $callback Value callback. Receives + * ($args, $block, $attribute_name); the + * helper has already injected + * $args['__airowp_post_id']. + * @param array $args_schema Optional arg schema for UI discovery. + */ + private static function register_one( Registry $registry, $slug, $label, array $returns, callable $callback, array $args_schema = array() ) { + airowp_register_bindings_source( + $slug, + $callback, + array( 'label' => $label ) + ); + + $registry->register_source( + $slug, + array( + 'label' => $label, + 'group' => 'post', + 'returns' => $returns, + 'args' => $args_schema, + ) + ); + } +} diff --git a/includes/Blocks/Common/DynamicTags/Registry.php b/includes/Blocks/Common/DynamicTags/Registry.php new file mode 100644 index 0000000..171f741 --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/Registry.php @@ -0,0 +1,267 @@ +> + */ + private $sources = array(); + + /** + * Registered groups keyed by slug. + * + * @var array + */ + private $groups = array(); + + /** + * Returns the singleton instance. + * + * @return Registry + */ + public static function instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + + if ( did_action( 'after_setup_theme' ) ) { + self::$instance->register_default_groups(); + } else { + add_action( + 'after_setup_theme', + function () { + Registry::instance()->register_default_groups(); + } + ); + } + } + return self::$instance; + } + + /** + * Seed the default group list so sources can reference them. + * + * Called from after_setup_theme so __() calls don't trigger + * _load_textdomain_just_in_time before translations are ready. + * Runs well before sources register at init priority 6. + */ + private function register_default_groups() { + $this->register_group( 'post', __( 'Post', 'airo-wp' ), 10 ); + $this->register_group( 'site', __( 'Site', 'airo-wp' ), 20 ); + $this->register_group( 'archive', __( 'Archive', 'airo-wp' ), 30 ); + $this->register_group( 'user', __( 'User', 'airo-wp' ), 40 ); + $this->register_group( 'custom-fields', __( 'Custom Fields', 'airo-wp' ), 50 ); + } + + /** + * Registers (or re-orders) a source group. + * + * @param string $slug Group slug, e.g. 'post'. + * @param string $label Display label. + * @param int $order Sort order (lower first). + */ + public function register_group( $slug, $label, $order = 100 ) { + $this->groups[ $slug ] = array( + 'slug' => $slug, + 'label' => $label, + 'order' => (int) $order, + ); + } + + /** + * Registers a Dynamic Tag source. + * + * @param string $slug Source slug (must match the slug passed to + * airowp_register_bindings_source()). + * @param array $meta { + * Source metadata. + * + * @type string $label Human-readable label. + * @type string $group Group slug (must be registered). + * @type string[] $returns Return types: text|image|url|number|date. + * @type array $args Arg schema keyed by arg name. + * @type callable $discovery_cb Optional callback returning + * an array of fields for field discovery. + * Signature: function( array $context ): array. + * @type callable $resolver Optional direct resolver. When present, + * the image resolver prefers this over + * round-tripping the core Bindings API. + * Signature: function( array $args, int $post_id ): mixed. + * @type string $capability Optional capability required to read. + * } + */ + public function register_source( $slug, array $meta ) { + $defaults = array( + 'label' => $slug, + 'group' => 'post', + 'returns' => array( 'text' ), + 'args' => array(), + 'discovery_cb' => null, + 'resolver' => null, + 'capability' => '', + ); + $this->sources[ $slug ] = array_merge( $defaults, $meta, array( 'slug' => $slug ) ); + } + + /** + * Returns all registered sources as a list. + * + * @param array $filters Optional filters: 'returns' (string|string[]), + * 'group' (string). + * @return array> + */ + public function all_sources( array $filters = array() ) { + $sources = array_values( $this->sources ); + + if ( isset( $filters['group'] ) && '' !== $filters['group'] ) { + $group = (string) $filters['group']; + $sources = array_values( + array_filter( + $sources, + static function ( $source ) use ( $group ) { + return $source['group'] === $group; + } + ) + ); + } + + if ( isset( $filters['returns'] ) ) { + $wanted = (array) $filters['returns']; + $sources = array_values( + array_filter( + $sources, + static function ( $source ) use ( $wanted ) { + $returns = (array) ( $source['returns'] ?? array() ); + foreach ( $wanted as $type ) { + if ( in_array( $type, $returns, true ) ) { + return true; + } + } + return false; + } + ) + ); + } + + return $sources; + } + + /** + * Returns a single source by slug or null. + * + * @param string $slug Source slug. + * @return array|null + */ + public function get_source( $slug ) { + return $this->sources[ $slug ] ?? null; + } + + /** + * Returns the group list sorted by order. + * + * @return array + */ + public function all_groups() { + $groups = array_values( $this->groups ); + usort( + $groups, + static function ( $a, $b ) { + return $a['order'] <=> $b['order']; + } + ); + return $groups; + } + + /** + * Runs a source's field-discovery callback if registered. + * + * @param string $slug Source slug. + * @param array $context Context array (post_type, post_id, returns). + * @return array> Fields discovered; empty on failure. + */ + public function discover_fields( $slug, array $context = array() ) { + $source = $this->get_source( $slug ); + if ( null === $source || ! is_callable( $source['discovery_cb'] ?? null ) ) { + return array(); + } + $result = call_user_func( $source['discovery_cb'], $context ); + return is_array( $result ) ? $result : array(); + } + + /** + * Resolves a source value for a given post context. + * + * Prefers a direct resolver when registered, otherwise falls back to + * the core Bindings source's get_value_callback (which is the wrapped + * closure from airowp_register_bindings_source() and therefore + * applies the shared password / viewable / protected-meta gates). + * + * NOTE ON DIRECT RESOLVERS: when a source registers its own + * $source['resolver'], this method invokes it without the shared + * gates. Callers registering a direct resolver MUST apply their own + * password / viewability / protected-meta checks — or, more commonly, + * omit the resolver and rely on the Bindings callback path. Callers + * of this method (e.g. RestController::get_preview) must also apply + * the password / viewable gate before invoking resolve(). + * + * @param string $slug Source slug. + * @param array $args Source args. + * @param int $post_id Post ID context. + * @return mixed|null Resolved value or null if unresolvable. + */ + public function resolve( $slug, array $args, $post_id = 0 ) { + $source = $this->get_source( $slug ); + if ( null === $source ) { + return null; + } + + // Capability gate for sensitive sources. + if ( ! empty( $source['capability'] ) && ! current_user_can( (string) $source['capability'] ) ) { + return null; + } + + if ( is_callable( $source['resolver'] ?? null ) ) { + return call_user_func( $source['resolver'], $args, (int) $post_id ); + } + + // Fall back to the core Bindings API value callback. + if ( function_exists( 'get_block_bindings_source' ) ) { + $binding = get_block_bindings_source( $slug ); + if ( $binding && isset( $binding->get_value_callback ) && is_callable( $binding->get_value_callback ) ) { + $args['__airowp_post_id'] = (int) $post_id; + return call_user_func( $binding->get_value_callback, $args, null, 'content' ); + } + } + + return null; + } +} diff --git a/includes/Blocks/Common/DynamicTags/RestController.php b/includes/Blocks/Common/DynamicTags/RestController.php new file mode 100644 index 0000000..1d1d1f3 --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/RestController.php @@ -0,0 +1,288 @@ +registry = $registry; + } + + /** + * Hook into rest_api_init. + */ + public function register_hooks() { + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Registers all three routes. + */ + public function register_routes() { + register_rest_route( + self::NS, + '/dynamic-tags/sources', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_sources' ), + 'permission_callback' => array( $this, 'permission_check' ), + 'args' => array( + 'returns' => array( + 'type' => 'string', + 'description' => __( 'Comma-separated return types to filter by (text|image|url|number|date).', 'airo-wp' ), + ), + 'postType' => array( + 'type' => 'string', + 'description' => __( 'Post type context for field discovery.', 'airo-wp' ), + ), + ), + ) + ); + + register_rest_route( + self::NS, + '/dynamic-tags/fields', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_fields' ), + 'permission_callback' => array( $this, 'permission_check' ), + 'args' => array( + 'source' => array( + 'type' => 'string', + 'required' => true, + ), + 'postType' => array( + 'type' => 'string', + 'default' => 'post', + ), + 'returns' => array( + 'type' => 'string', + ), + ), + ) + ); + + register_rest_route( + self::NS, + '/dynamic-tags/preview', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_preview' ), + 'permission_callback' => array( $this, 'permission_check' ), + 'args' => array( + 'source' => array( + 'type' => 'string', + 'required' => true, + ), + 'args' => array( + 'type' => 'object', + 'default' => array(), + ), + 'postId' => array( + 'type' => 'integer', + ), + 'size' => array( + 'type' => 'string', + 'default' => 'medium', + ), + ), + ) + ); + } + + /** + * Editor-baseline capability gate. + * + * @return bool + */ + public function permission_check() { + return current_user_can( 'edit_posts' ); + } + + /** + * GET /dynamic-tags/sources + * + * @param \WP_REST_Request $request Request. + * @return \WP_REST_Response + */ + public function get_sources( \WP_REST_Request $request ) { + $filters = array(); + $returns = $request->get_param( 'returns' ); + if ( is_string( $returns ) && '' !== $returns ) { + $filters['returns'] = array_values( + array_filter( + array_map( 'trim', explode( ',', $returns ) ) + ) + ); + } + + $sources = array_map( + static function ( $source ) { + return array( + 'slug' => $source['slug'], + 'label' => $source['label'], + 'group' => $source['group'], + 'returns' => array_values( (array) $source['returns'] ), + 'args' => (object) ( $source['args'] ?? array() ), + 'supportsFieldDiscovery' => is_callable( $source['discovery_cb'] ?? null ), + ); + }, + $this->registry->all_sources( $filters ) + ); + + return rest_ensure_response( + array( + 'groups' => array_values( $this->registry->all_groups() ), + 'sources' => $sources, + ) + ); + } + + /** + * GET /dynamic-tags/fields + * + * @param \WP_REST_Request $request Request. + * @return \WP_REST_Response + */ + public function get_fields( \WP_REST_Request $request ) { + $slug = sanitize_text_field( (string) $request->get_param( 'source' ) ); + $post_type = sanitize_key( (string) $request->get_param( 'postType' ) ); + $returns = sanitize_key( (string) $request->get_param( 'returns' ) ); + + $context = array( + 'post_type' => $post_type, + 'returns' => $returns, + ); + + $fields = $this->registry->discover_fields( $slug, $context ); + + // Strip protected meta keys defensively. + $fields = array_values( + array_filter( + $fields, + static function ( $field ) { + $key = isset( $field['key'] ) ? (string) $field['key'] : ''; + return '' !== $key && 0 !== strpos( $key, '_' ); + } + ) + ); + + return rest_ensure_response( array( 'fields' => $fields ) ); + } + + /** + * GET /dynamic-tags/preview + * + * @param \WP_REST_Request $request Request. + * @return \WP_REST_Response + */ + public function get_preview( \WP_REST_Request $request ) { + $slug = (string) $request->get_param( 'source' ); + $args = (array) $request->get_param( 'args' ); + $post_id = (int) $request->get_param( 'postId' ); + $size = (string) $request->get_param( 'size' ); + + $source = $this->registry->get_source( $slug ); + if ( ! $source ) { + return rest_ensure_response( + array( + 'status' => 'error', + 'error' => 'unknown_source', + ) + ); + } + + if ( $post_id ) { + $post = get_post( $post_id ); + if ( ! $post ) { + return rest_ensure_response( + array( + 'status' => 'error', + 'error' => 'unknown_post', + ) + ); + } + if ( post_password_required( $post ) || ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post_id ) ) ) { + return rest_ensure_response( array( 'status' => 'unauthorized' ) ); + } + } + + $returns = (array) ( $source['returns'] ?? array() ); + + if ( in_array( 'image', $returns, true ) ) { + $descriptor = ImageResolver::resolve( $slug, $args, $post_id, $size ); + if ( null === $descriptor ) { + return rest_ensure_response( + array( + 'status' => 'empty', + 'returns' => 'image', + ) + ); + } + return rest_ensure_response( + array( + 'status' => 'resolved', + 'returns' => 'image', + 'value' => $descriptor, + ) + ); + } + + // Scalar source — delegate to the core Bindings callback. + $value = $this->registry->resolve( $slug, $args, $post_id ); + if ( null === $value || '' === $value ) { + return rest_ensure_response( + array( + 'status' => 'empty', + 'returns' => $returns[0] ?? 'text', + ) + ); + } + + return rest_ensure_response( + array( + 'status' => 'resolved', + 'returns' => $returns[0] ?? 'text', + 'value' => $value, + ) + ); + } +} diff --git a/includes/Blocks/Common/DynamicTags/SiteSources.php b/includes/Blocks/Common/DynamicTags/SiteSources.php new file mode 100644 index 0000000..dd94c3d --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/SiteSources.php @@ -0,0 +1,115 @@ + array( + 'type' => 'string', + 'enum' => array( 'url', 'id', 'alt', 'width', 'height' ), + ), + 'size' => array( 'type' => 'string' ), + ) + ); + } + + /** + * Registers one source with both core Bindings and our metadata registry. + * + * @param Registry $registry Metadata registry. + * @param string $slug Binding source slug. + * @param string $label Display label. + * @param string[] $returns Return types. + * @param callable $callback Value callback. + * @param array $args_schema Optional arg schema. + */ + private static function register_one( Registry $registry, $slug, $label, array $returns, callable $callback, array $args_schema = array() ) { + airowp_register_bindings_source( + $slug, + $callback, + array( 'label' => $label ) + ); + $registry->register_source( + $slug, + array( + 'label' => $label, + 'group' => 'site', + 'returns' => $returns, + 'args' => $args_schema, + ) + ); + } +} diff --git a/includes/Blocks/Common/DynamicTags/UserSources.php b/includes/Blocks/Common/DynamicTags/UserSources.php new file mode 100644 index 0000000..cb396b7 --- /dev/null +++ b/includes/Blocks/Common/DynamicTags/UserSources.php @@ -0,0 +1,146 @@ +ID ) { + return null; + } + return (string) $user->display_name; + } + ); + + self::register_one( + $registry, + 'airo-wp/current-user-avatar', + __( 'Current user avatar', 'airo-wp' ), + array( 'image', 'url' ), + static function ( $args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $user = wp_get_current_user(); + if ( 0 === (int) $user->ID ) { + return null; + } + $size = isset( $args['size'] ) ? (int) $args['size'] : 96; + $url = get_avatar_url( $user->ID, array( 'size' => max( 24, $size ) ) ); + return $url ? (string) $url : null; + }, + array( + 'size' => array( 'type' => 'integer' ), + ) + ); + + self::register_one( + $registry, + 'airo-wp/current-user-url', + __( 'Current user website URL', 'airo-wp' ), + array( 'url' ), + static function ( $_args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + $user = wp_get_current_user(); + if ( 0 === (int) $user->ID ) { + return null; + } + // The profile URL is user-editable (contributors can set their + // own). Core's renderer escapes it, but this value also flows + // raw to the /dynamic-tags/preview REST JSON — enforce an + // http/https allowlist so a `javascript:` URL can never reach a + // consumer that assigns it to el.href. + $url = esc_url_raw( (string) $user->user_url, array( 'http', 'https' ) ); + return '' === $url ? null : $url; + } + ); + + // Always register the email source so plugins hooking `init` at the + // default priority (10) can still enable it; the value callback + // evaluates `airowp_dynamic_tags_allow_email` lazily at render + // time. Disabled by default because exposing an email on the public + // frontend is almost always a privacy mistake. + self::register_one( + $registry, + 'airo-wp/current-user-email', + __( 'Current user email', 'airo-wp' ), + array( 'text' ), + static function ( $_args, $_block, $_attr ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by register_block_bindings_source() callback signature + /** + * Enables the `airo-wp/current-user-email` source. + * + * @since 2.2.0 + * + * @param bool $enabled Whether to resolve the email source. + */ + if ( ! apply_filters( 'airowp_dynamic_tags_allow_email', false ) ) { + return null; + } + $user = wp_get_current_user(); + if ( 0 === (int) $user->ID ) { + return null; + } + return (string) $user->user_email; + }, + array(), + 'read' + ); + } + + /** + * Registers one source with both core Bindings and our metadata registry. + * + * @param Registry $registry Metadata registry. + * @param string $slug Binding source slug. + * @param string $label Display label. + * @param string[] $returns Return types. + * @param callable $callback Value callback. + * @param array $args_schema Optional arg schema. + * @param string $capability Optional capability gate. + */ + private static function register_one( Registry $registry, $slug, $label, array $returns, callable $callback, array $args_schema = array(), $capability = '' ) { + airowp_register_bindings_source( + $slug, + $callback, + array( 'label' => $label ) + ); + $registry->register_source( + $slug, + array( + 'label' => $label, + 'group' => 'user', + 'returns' => $returns, + 'args' => $args_schema, + 'capability' => $capability, + ) + ); + } +} diff --git a/includes/Blocks/Common/Forms/FormHandler.php b/includes/Blocks/Common/Forms/FormHandler.php new file mode 100644 index 0000000..520134c --- /dev/null +++ b/includes/Blocks/Common/Forms/FormHandler.php @@ -0,0 +1,1541 @@ + 10 ) { + * // Add to blocklist + * update_option( 'blocked_ips', array_merge( + * get_option( 'blocked_ips', [] ), + * [ $ip ] + * ) ); + * } + * }, 10, 3 ); + * ``` + */ +class FormHandler { + + /** + * Security module for spam checks, rate limiting, and verification. + * + * @var FormSecurity + */ + private FormSecurity $security; + + /** + * Constructor. + */ + public function __construct() { + $this->security = new FormSecurity(); + + add_action( 'rest_api_init', array( $this, 'register_rest_endpoint' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'localize_form_script' ) ); + + // Admin-ajax fallback for hosts that rate-limit /wp-json/ (e.g. Cloudflare/GoDaddy). + add_action( 'wp_ajax_airowp_form_submit', array( $this, 'handle_ajax_submission' ) ); + add_action( 'wp_ajax_nopriv_airowp_form_submit', array( $this, 'handle_ajax_submission' ) ); + + // Non-AJAX form POST handler. + add_action( 'admin_post_airowp_form_submit', array( $this, 'handle_post_submission' ) ); + add_action( 'admin_post_nopriv_airowp_form_submit', array( $this, 'handle_post_submission' ) ); + + // Register cron callback (scheduling handled by activation hook). + add_action( 'airowp_cleanup_old_submissions', array( $this, 'cleanup_old_submissions' ) ); + + // Invalidate cached form block attributes when posts are saved. + add_action( 'save_post', array( $this, 'clear_form_attributes_cache' ) ); + } + + /** + * Get form settings. + * + * @return array Form settings with defaults applied. + */ + private function get_form_settings() { + $settings = get_option( 'airowp_settings', array() ); + $defaults = array( + 'enable_honeypot' => true, + 'enable_rate_limiting' => true, + 'retention_days' => 30, + ); + + return isset( $settings['forms'] ) ? wp_parse_args( $settings['forms'], $defaults ) : $defaults; + } + + /** + * Register REST API endpoint for form submission. + * + * SECURITY NOTE: This is a public endpoint (permission_callback = __return_true) + * because it needs to accept form submissions from non-logged-in users. + * + * Security measures in place: + * - Honeypot field check (detects spam bots) - configurable via settings + * - Time-based submission check (< 3 seconds = likely bot) + * - Rate limiting (3 submissions per 60 seconds per IP address) - configurable via settings + * - Comprehensive field validation (email, url, phone, number types) + * - Type-specific sanitization for all field values + * - Server-side email configuration lookup (email settings are never sent from the client) + * - Email header injection prevention (defense in depth) + * + * Additional protection available: + * - Cloudflare Turnstile integration (configurable per-form) + * - More aggressive rate limiting via filters + * - IP blocklist functionality via security monitoring hooks + * + * Extensibility: + * - Use 'airowp_form_rate_limit_count' filter to adjust rate limit + * - Use 'airowp_form_rate_limit_window' filter to adjust time window + * - Use 'airowp_form_submitted' action to hook into successful submissions + */ + public function register_rest_endpoint() { + register_rest_route( + 'airo-wp/v1', + '/form/submit', + array( + 'methods' => 'POST', + 'callback' => array( $this, 'handle_form_submission' ), + 'permission_callback' => '__return_true', // Public endpoint - see DocBlock above for security measures. + 'args' => array( + 'formId' => array( + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => function ( $param ) { + return is_string( $param ) && ! empty( $param ); + }, + ), + 'fields' => array( + 'required' => true, + 'type' => 'array', + 'validate_callback' => function ( $param ) { + return is_array( $param ); + }, + ), + 'honeypot' => array( + 'required' => false, + 'type' => 'string', + 'default' => '', + ), + 'timestamp' => array( + 'required' => false, + 'type' => 'string', + 'default' => '', + ), + 'turnstile_token' => array( + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => function ( $value ) { + // Empty is valid (graceful degradation). + if ( empty( $value ) ) { + return true; + } + // Turnstile tokens are alphanumeric with hyphens/underscores. + if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $value ) ) { + return new \WP_Error( + 'invalid_turnstile_token', + __( 'Invalid Turnstile token format.', 'airo-wp' ) + ); + } + return true; + }, + ), + ), + ) + ); + } + + /** + * Handle form submission. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error on failure. + */ + public function handle_form_submission( $request ) { + // CSRF Protection: Verify nonce when one is supplied. + // + // Rationale: WordPress only emits a valid `wp_rest` nonce for users with + // an active session — fully anonymous visitors hit this endpoint without + // one. We can't require a nonce universally without breaking unauthenticated + // public-facing forms (the primary use case for this block). Instead, we + // layer non-cookie defences for anonymous submissions: + // 1. Honeypot field (rejects most bots). + // 2. Submission-timing check (rejects sub-3s autofill). + // 3. IP rate limiting (default 2/60s, configurable per-form). + // 4. Optional Cloudflare Turnstile (recommended for high-value forms). + // For sensitive use cases, enable Turnstile in the block's Inspector. + $nonce = $request->get_header( 'X-WP-Nonce' ); + if ( $nonce && ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { + return new WP_Error( + 'invalid_nonce', + __( 'Security verification failed. Please refresh the page and try again.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + $form_id = $request->get_param( 'formId' ); + $fields = $request->get_param( 'fields' ); + $honeypot = $request->get_param( 'honeypot' ); + $timestamp = $request->get_param( 'timestamp' ); + $form_settings = $this->get_form_settings(); + + // Look up per-block attributes for rate limiting and Turnstile enforcement. + // Fallback (2/60s) is intentionally stricter than the block-level default + // — it only applies to orphaned/legacy submissions where the per-block + // configuration is missing. + $block_attrs = $this->get_form_block_attributes( $form_id ); + $rate_limit_count = isset( $block_attrs['rateLimitCount'] ) ? absint( $block_attrs['rateLimitCount'] ) : 2; + $rate_limit_window = isset( $block_attrs['rateLimitWindow'] ) ? absint( $block_attrs['rateLimitWindow'] ) : 60; + $turnstile_required = ! empty( $block_attrs['enableTurnstile'] ); + + // Honeypot spam check (only if enabled in settings). + if ( $form_settings['enable_honeypot'] ) { + $honeypot_check = $this->security->check_honeypot( $honeypot, $form_id ); + if ( is_wp_error( $honeypot_check ) ) { + return $honeypot_check; + } + } + + // Time-based spam check. + $timing_check = $this->security->check_submission_timing( $timestamp, $form_id ); + if ( is_wp_error( $timing_check ) ) { + return $timing_check; + } + + // Rate limiting check (only if enabled in settings). + // Uses per-block rateLimitCount/rateLimitWindow attributes as defaults for the filter. + if ( $form_settings['enable_rate_limiting'] ) { + $rate_limit_check = $this->security->check_rate_limit( $form_id, $rate_limit_count ); + if ( is_wp_error( $rate_limit_check ) ) { + return $rate_limit_check; + } + } + + // Turnstile verification. + // If the block requires Turnstile, reject submissions without a valid token. + $turnstile_token = $request->get_param( 'turnstile_token' ); + if ( $turnstile_required && empty( $turnstile_token ) ) { + return new WP_Error( + 'turnstile_required', + __( 'Security verification is required. Please complete the challenge and try again.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + if ( ! empty( $turnstile_token ) ) { + $turnstile_result = $this->security->verify_turnstile( $turnstile_token ); + if ( is_wp_error( $turnstile_result ) ) { + /** + * Fires when Cloudflare Turnstile verification fails. + * + * @since 1.0.0 + * @param string $form_id Form identifier. + * @param string $ip_address Client IP address. + * @param string $error_code Error code from verification. + */ + do_action( 'airowp_form_turnstile_failed', $form_id, $this->security->get_client_ip(), $turnstile_result->get_error_code() ); + return $turnstile_result; + } + } + + // Sanitize and validate all fields. + $form_field_types = $this->get_form_field_types( $form_id ); + $field_constraints = $this->get_form_field_value_constraints( $form_id ); + $sanitized_fields = array(); + foreach ( $fields as $field ) { + if ( ! isset( $field['name'] ) || ! isset( $field['value'] ) ) { + continue; + } + + $field_name = sanitize_text_field( $field['name'] ); + $field_value = $field['value']; + $submitted_field_type = isset( $field['type'] ) ? sanitize_text_field( $field['type'] ) : 'text'; + $field_type = isset( $form_field_types[ $field_name ] ) + ? $form_field_types[ $field_name ] + : $submitted_field_type; + + // Server-defined allowed values for constrained field types (only + // present for select/checkbox/hidden fields resolved from the block). + $allowed_values = isset( $field_constraints[ $field_name ] ) + ? $field_constraints[ $field_name ] + : null; + + // Type-specific validation. + $validation_result = $this->validate_field( $field_value, $field_type, $allowed_values ); + if ( is_wp_error( $validation_result ) ) { + // Security monitoring hook for validation failures. + do_action( 'airowp_form_validation_failed', $form_id, $field_name, $field_type, $validation_result->get_error_code(), $this->security->get_client_ip() ); + + return new WP_Error( + 'validation_error', + sprintf( + /* translators: %1$s: field name, %2$s: error message */ + __( 'Field "%1$s": %2$s', 'airo-wp' ), + $field_name, + $validation_result->get_error_message() + ), + array( 'status' => 400 ) + ); + } + + // Type-specific sanitization. + $sanitized_value = $this->sanitize_field( $field_value, $field_type ); + + $sanitized_fields[ $field_name ] = array( + 'value' => $sanitized_value, + 'type' => $field_type, + ); + } + + // Store submission. + $submission_id = $this->store_submission( $form_id, $sanitized_fields ); + + if ( is_wp_error( $submission_id ) ) { + return new WP_Error( + 'submission_failed', + __( 'Failed to save form submission. Please try again.', 'airo-wp' ), + array( 'status' => 500 ) + ); + } + + // Send email notification if enabled (settings looked up server-side from block attributes). + $this->send_email_notification( $form_id, $sanitized_fields, $submission_id ); + + // Increment rate limit counter ONLY after successful submission. + if ( $form_settings['enable_rate_limiting'] ) { + $this->security->increment_rate_limit( $form_id, $rate_limit_window ); + } + + // Trigger action hook for email notifications, integrations, etc. + do_action( 'airowp_form_submitted', $submission_id, $form_id, $sanitized_fields ); + + return new WP_REST_Response( + array( + 'success' => true, + 'message' => __( 'Form submitted successfully!', 'airo-wp' ), + 'submissionId' => $submission_id, + ), + 200 + ); + } + + /** + * Handle form submission via admin-ajax.php (fallback for rate-limited REST API). + * + * Wraps the REST handler by building a WP_REST_Request from $_POST data. + */ + public function handle_ajax_submission() { + // Verify nonce. + if ( ! check_ajax_referer( 'airowp_form_submit', '_ajax_nonce', false ) ) { + wp_send_json_error( + array( 'message' => __( 'Security verification failed. Please refresh the page and try again.', 'airo-wp' ) ), + 403 + ); + } + + $request = new \WP_REST_Request( 'POST' ); + $request->set_header( 'Content-Type', 'application/json' ); + + // Read JSON form data from the form_data POST field (form-encoded). + // Form-encoded is used because some hosts (GoDaddy/Cloudflare) block + // application/json POST requests with 429 errors. + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by handle_form_submission. + $raw_data = isset( $_POST['form_data'] ) ? wp_unslash( $_POST['form_data'] ) : ''; + $data = json_decode( $raw_data, true ); + + if ( ! is_array( $data ) ) { + wp_send_json_error( + array( 'message' => __( 'Invalid request data.', 'airo-wp' ) ), + 400 + ); + } + + $request->set_param( 'formId', isset( $data['formId'] ) ? sanitize_text_field( $data['formId'] ) : '' ); + $request->set_param( 'fields', isset( $data['fields'] ) ? $data['fields'] : array() ); + $request->set_param( 'honeypot', isset( $data['honeypot'] ) ? $data['honeypot'] : '' ); + $request->set_param( 'timestamp', isset( $data['timestamp'] ) ? $data['timestamp'] : '' ); + $request->set_param( 'turnstile_token', isset( $data['turnstile_token'] ) ? sanitize_text_field( $data['turnstile_token'] ) : '' ); + + $result = $this->handle_form_submission( $request ); + + if ( is_wp_error( $result ) ) { + $status = $result->get_error_data() && isset( $result->get_error_data()['status'] ) + ? $result->get_error_data()['status'] + : 400; + wp_send_json_error( + array( 'message' => $result->get_error_message() ), + $status + ); + } + + wp_send_json_success( $result->get_data() ); + } + + /** + * Handle non-AJAX form submission via admin_post. + * + * Processes standard form POST and redirects back with a status query param. + */ + public function handle_post_submission() { + // Verify nonce. + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'airowp_form_submit' ) ) { + wp_die( esc_html__( 'Security verification failed.', 'airo-wp' ), '', array( 'response' => 403 ) ); + } + + $referer = wp_get_referer(); + if ( ! $referer ) { + wp_die( esc_html__( 'Invalid form submission.', 'airo-wp' ), '', array( 'response' => 400 ) ); + } + + // Build fields array from POST data. + $form_id = isset( $_POST['dsg_form_id'] ) ? sanitize_text_field( wp_unslash( $_POST['dsg_form_id'] ) ) : ''; + $fields = array(); + + // Read field type map (JSON mapping of field name => type). + $field_types = array(); + if ( isset( $_POST['dsg_field_types'] ) ) { + $decoded = json_decode( sanitize_text_field( wp_unslash( $_POST['dsg_field_types'] ) ), true ); + if ( is_array( $decoded ) ) { + $field_types = $decoded; + } + } + + $system_fields = array( 'dsg_website', 'dsg_form_id', 'dsg_timestamp', 'dsg_turnstile_token', 'dsg_field_types', '_wpnonce', 'action' ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above. + foreach ( $_POST as $key => $value ) { + // Skip system fields. + if ( in_array( $key, $system_fields, true ) ) { + continue; + } + + $field_type = isset( $field_types[ $key ] ) ? sanitize_text_field( $field_types[ $key ] ) : 'text'; + + $fields[] = array( + 'name' => $key, + 'value' => sanitize_text_field( wp_unslash( $value ) ), + 'type' => $field_type, + ); + } + + $request = new \WP_REST_Request( 'POST' ); + $request->set_param( 'formId', $form_id ); + $request->set_param( 'fields', $fields ); + $request->set_param( 'honeypot', isset( $_POST['dsg_website'] ) ? sanitize_text_field( wp_unslash( $_POST['dsg_website'] ) ) : '' ); + $request->set_param( 'timestamp', isset( $_POST['dsg_timestamp'] ) ? sanitize_text_field( wp_unslash( $_POST['dsg_timestamp'] ) ) : '' ); + $request->set_param( 'turnstile_token', isset( $_POST['dsg_turnstile_token'] ) ? sanitize_text_field( wp_unslash( $_POST['dsg_turnstile_token'] ) ) : '' ); + + $result = $this->handle_form_submission( $request ); + + if ( is_wp_error( $result ) ) { + $redirect = add_query_arg( + array( + 'airowp_form_status' => 'error', + 'airowp_form_id' => $form_id, + ), + $referer + ); + } else { + $redirect = add_query_arg( + array( + 'airowp_form_status' => 'success', + 'airowp_form_id' => $form_id, + ), + $referer + ); + } + + wp_safe_redirect( $redirect ); + exit; + } + + /** + * Validate field based on type. + * + * @param mixed $value Field value. + * @param string $type Field type. + * @param array|null $allowed Optional list of server-defined allowed values + * (for select/checkbox/hidden). When provided, the + * submitted value(s) must be members of this list. + * @return true|WP_Error True if valid, WP_Error if invalid. + */ + private function validate_field( $value, $type, $allowed = null ) { + // Skip validation for empty values (optional fields). + // Required fields are validated by HTML5 on the frontend. + if ( empty( $value ) && '0' !== $value ) { + return true; + } + + // Enforce server-defined allowed values for constrained field types + // (select options, checkbox value, hidden constant). This prevents a + // client from submitting arbitrary values or forging hidden fields — + // the field type itself is already resolved server-side. + if ( is_array( $allowed ) ) { + $submitted = is_array( $value ) ? $value : array( $value ); + foreach ( $submitted as $single ) { + if ( ! in_array( (string) $single, $allowed, true ) ) { + return new WP_Error( + 'value_not_allowed', + __( 'Submitted value is not an allowed option.', 'airo-wp' ) + ); + } + } + } + + switch ( $type ) { + case 'email': + if ( ! is_email( $value ) ) { + return new WP_Error( + 'invalid_email', + __( 'Invalid email address.', 'airo-wp' ) + ); + } + break; + + case 'url': + if ( ! filter_var( $value, FILTER_VALIDATE_URL ) || ! preg_match( '/^https?:\/\//i', $value ) ) { + return new WP_Error( + 'invalid_url', + __( 'Invalid URL.', 'airo-wp' ) + ); + } + break; + + case 'number': + if ( ! is_numeric( $value ) ) { + return new WP_Error( + 'invalid_number', + __( 'Invalid number.', 'airo-wp' ) + ); + } + break; + + case 'tel': + // Basic phone validation - numbers, spaces, dashes, parentheses, plus. + if ( ! preg_match( '/^[0-9\s\-\(\)\+]+$/', $value ) ) { + return new WP_Error( + 'invalid_phone', + __( 'Invalid phone number.', 'airo-wp' ) + ); + } + break; + } + + return true; + } + + /** + * Sanitize field based on type. + * + * @param mixed $value Field value. + * @param string $type Field type. + * @return mixed Sanitized value. + */ + private function sanitize_field( $value, $type ) { + switch ( $type ) { + case 'email': + return sanitize_email( $value ); + + case 'url': + return esc_url_raw( $value ); + + case 'number': + return is_numeric( $value ) ? floatval( $value ) : 0; + + case 'tel': + return preg_replace( '/[^0-9\s\-\(\)\+]/', '', $value ); + + case 'textarea': + return sanitize_textarea_field( $value ); + + case 'text': + default: + return sanitize_text_field( $value ); + } + } + + /** + * Store form submission as custom post type. + * + * @param string $form_id Form ID. + * @param array $fields Sanitized fields array. + * @return int|WP_Error Post ID on success, WP_Error on failure. + */ + private function store_submission( $form_id, $fields ) { + $post_id = wp_insert_post( + array( + 'post_type' => 'dsgo_form_submission', + 'post_status' => 'private', + 'post_title' => sprintf( + /* translators: %s: form ID */ + __( 'Form Submission - %s', 'airo-wp' ), + $form_id + ), + 'post_date' => current_time( 'mysql' ), + ), + true + ); + + if ( is_wp_error( $post_id ) ) { + return $post_id; + } + + // Store form data as post meta. + // + // update_metadata() runs wp_unslash() on every value it stores, so it expects + // SLASHED input. Everything below has already been unslashed (REST params + // arrive unslashed; the no-JS $_POST path unslashes explicitly), so passing + // these straight through would strip one level of escaping and silently eat + // backslashes out of submitted content — "C:\Users\me" would land as + // "C:Usersme". Re-slash the user-controlled values to survive that unslash. + // wp_slash() walks arrays, so $fields is covered in full. + update_post_meta( $post_id, '_dsg_form_id', $form_id ); + update_post_meta( $post_id, '_dsg_form_fields', wp_slash( $fields ) ); + update_post_meta( $post_id, '_dsg_submission_ip', $this->security->get_client_ip() ); + update_post_meta( $post_id, '_dsg_submission_user_agent', wp_slash( $this->get_user_agent() ) ); + update_post_meta( $post_id, '_dsg_submission_referer', wp_slash( wp_get_referer() ) ); + update_post_meta( $post_id, '_dsg_submission_date', current_time( 'mysql' ) ); + + // Clear cached form submission count. + delete_transient( 'dsgo_form_submissions_count' ); + + return $post_id; + } + + /** + * Get user agent string. + * + * @return string User agent. + */ + private function get_user_agent() { + return isset( $_SERVER['HTTP_USER_AGENT'] ) + ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) + : 'unknown'; + } + + /** + * Localize script with nonce and REST URL. + */ + public function localize_form_script() { + // Only enqueue if form block is present on the page. + if ( ! has_block( 'airo-wp/form-builder' ) ) { + return; + } + + // Get the form-builder view script handle. + $asset_file = include AIRO_WP_PLUGIN_DIR . 'build/blocks/form-builder/view.asset.php'; + $handle = 'airo-wp-form-builder-view-script'; + + // Localize with nonce and REST URL. + wp_localize_script( + $handle, + 'airowpForm', + array( + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'restUrl' => rest_url( 'airo-wp/v1/form/submit' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'adminPostUrl' => admin_url( 'admin-post.php' ), + 'ajaxNonce' => wp_create_nonce( 'airowp_form_submit' ), + ) + ); + + // Localize integrations settings for Turnstile. + $settings = get_option( 'airowp_settings', array() ); + $integrations_settings = isset( $settings['integrations'] ) ? $settings['integrations'] : array(); + + wp_localize_script( + $handle, + 'dsgoIntegrations', + array( + 'turnstileSiteKey' => ! empty( $integrations_settings['turnstile_site_key'] ) ? esc_js( $integrations_settings['turnstile_site_key'] ) : '', + ) + ); + } + + /** + * Flatten a submitted field to a string. + * + * A field arrives either as a bare scalar or as `array( 'value' => … )`, and + * that value is itself an array for a multi-value field (checkbox group, + * multi-select) — which esc_html() would raise on. + * + * The whole point of this helper is that EVERY shape is safe to stringify, so + * it must not assume the shape it is handed. A submission is attacker-shaped + * data: an array with no `value` key, or one nesting arrays inside arrays, + * would make a plain strval() emit an "Array to string conversion" warning. + * Flattening recurses instead. + * + * @param mixed $field_data Raw submitted field. + * @return string Flattened value. + */ + private static function stringify_field_value( $field_data ) { + $value = ( is_array( $field_data ) && array_key_exists( 'value', $field_data ) ) + ? $field_data['value'] + : $field_data; + + return self::flatten_value( $value ); + } + + /** + * Recursively reduce any value to a display string. + * + * @param mixed $value Value of arbitrary shape. + * @return string Flattened value. + */ + private static function flatten_value( $value ) { + if ( is_array( $value ) ) { + return implode( ', ', array_map( array( self::class, 'flatten_value' ), $value ) ); + } + + if ( is_bool( $value ) ) { + return $value ? '1' : ''; + } + + if ( null === $value || is_scalar( $value ) ) { + return (string) $value; + } + + // Objects/resources have no meaningful representation in an email. + return ''; + } + + /** + * Send email notification with form submission data. + * + * Email configuration is read from the server-side block attributes (stored + * in post content), NOT from the client request. This prevents attackers from + * manipulating email recipients, sender addresses, or body content. + * + * @param string $form_id Form ID. + * @param array $fields Sanitized form fields. + * @param int $submission_id Submission post ID. + */ + private function send_email_notification( $form_id, $fields, $submission_id ) { + // Look up email settings from the form block attributes (server-side only). + // This prevents client-side manipulation of email configuration. + $block_attrs = $this->get_form_block_attributes( $form_id ); + + if ( ! $block_attrs || empty( $block_attrs['enableEmail'] ) ) { + if ( ! $block_attrs && defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging. + sprintf( 'airo-wp Form: Could not find block attributes for form ID "%s".', $form_id ) + ); + } + return; + } + + // Extract raw values from block attributes. + $email_to = isset( $block_attrs['emailTo'] ) ? $block_attrs['emailTo'] : ''; + $email_subject = isset( $block_attrs['emailSubject'] ) ? $block_attrs['emailSubject'] : ''; + $email_from_name = isset( $block_attrs['emailFromName'] ) ? $block_attrs['emailFromName'] : ''; + $email_from = isset( $block_attrs['emailFromEmail'] ) ? $block_attrs['emailFromEmail'] : ''; + $email_body = isset( $block_attrs['emailBody'] ) ? $block_attrs['emailBody'] : ''; + // emailReplyTo is a field NAME reference (e.g., "email"), not an email address. + // The actual email value is extracted from submitted fields and sanitized below (line ~650). + $email_reply_to = isset( $block_attrs['emailReplyTo'] ) ? $block_attrs['emailReplyTo'] : ''; + + // Strip newlines from header-used values to prevent injection (defense in depth). + $newline_chars = array( "\r", "\n", '%0a', '%0d' ); + $email_to = str_replace( $newline_chars, '', $email_to ); + $email_subject = str_replace( $newline_chars, '', $email_subject ); + $email_from_name = str_replace( $newline_chars, '', $email_from_name ); + $email_from = str_replace( $newline_chars, '', $email_from ); + $email_reply_to = str_replace( $newline_chars, '', $email_reply_to ); + + // Validate and set defaults (single sanitization pass). + if ( empty( $email_to ) || ! is_email( $email_to ) ) { + $email_to = get_option( 'admin_email' ); + } else { + $email_to = sanitize_email( $email_to ); + } + + if ( empty( $email_subject ) ) { + $email_subject = __( 'New Form Submission', 'airo-wp' ); + } else { + $email_subject = sanitize_text_field( $email_subject ); + } + + if ( empty( $email_from_name ) ) { + $email_from_name = get_bloginfo( 'name' ); + } else { + $email_from_name = sanitize_text_field( $email_from_name ); + } + + if ( empty( $email_from ) ) { + // Use domain-matched email address for better deliverability. + // This matches WordPress core and other plugins like CoBlocks. + $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST ); + if ( null !== $sitename ) { + // Remove www prefix if present. + if ( 'www.' === substr( $sitename, 0, 4 ) ) { + $sitename = substr( $sitename, 4 ); + } + $email_from = 'wordpress@' . $sitename; + } else { + // Fallback to admin email if we can't parse the domain. + $email_from = get_option( 'admin_email' ); + } + } else { + $email_from = sanitize_email( $email_from ); + if ( ! is_email( $email_from ) ) { + // Use domain-matched email address as fallback. + $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST ); + if ( null !== $sitename ) { + if ( 'www.' === substr( $sitename, 0, 4 ) ) { + $sitename = substr( $sitename, 4 ); + } + $email_from = 'wordpress@' . $sitename; + } else { + $email_from = get_option( 'admin_email' ); + } + } + } + + // Prepare merge tags. + $current_url = ''; + if ( isset( $_SERVER['REQUEST_URI'] ) ) { + $current_url = esc_url_raw( + home_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) + ); + } + + /* + * Merge tags land in TWO places with different rules, so they need two + * maps. Sharing one is what made the first cut of this fix wrong. + * + * BODY is HTML (`Content-Type: text/html`), so values MUST be esc_html()'d + * or a submitter puts arbitrary markup in the owner's inbox. + * + * SUBJECT is a plain-text mail header. It is never parsed as markup, so + * escaping it is not protection, it is corruption: a site named + * "Bob's Bakery & Sons" would arrive as "Bob's Bakery & Sons". + * What the subject actually needs is newline stripping, or a submitted + * value could inject a second mail header. + * + * The subject is a documented merge-tag target ("Use {field_name} for + * dynamic values" in the Form Builder inspector), so this is a real + * authoring surface, not a theoretical one. + */ + // WordPress stores `blogname` ALREADY HTML-escaped — sanitize_option() runs + // esc_html() on save — so get_bloginfo('name') hands back + // `Bob's Bakery & Sons`. Decoding it here is what makes these + // genuinely raw values, and is the same thing core does when it puts the + // site name into an email (see wp_new_user_notification()). The body map + // re-escapes it below; the subject, being plain text, wants it as typed. + $raw_values = array( + '{form_id}' => (string) $form_id, + '{submission_id}' => (string) $submission_id, + '{page_url}' => (string) $current_url, + '{site_name}' => wp_specialchars_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES ), + '{date}' => (string) current_time( 'mysql' ), + ); + + foreach ( $fields as $field_name => $field_data ) { + $raw_values[ '{' . $field_name . '}' ] = self::stringify_field_value( $field_data ); + } + + // The {all_fields} summary is markup by construction, so it is built once + // per context rather than escaped generically: HTML for the body, and a + // flat one-liner for the subject (where tags would be nonsense). + $all_fields_html = ''; + $all_fields_text = array(); + foreach ( $fields as $field_name => $field_data ) { + $value = self::stringify_field_value( $field_data ); + $label = ucwords( str_replace( array( '_', '-' ), ' ', $field_name ) ); + + $all_fields_html .= sprintf( "%s: %s
\n", esc_html( $label ), esc_html( $value ) ); + $all_fields_text[] = $label . ': ' . $value; + } + + // Body: escape every value. {all_fields} is already escaped internally. + $body_tags = array_map( 'esc_html', $raw_values ); + $body_tags['{all_fields}'] = $all_fields_html; + + // Subject: do NOT escape. Strip newlines so a submitted value cannot + // smuggle in an extra mail header (PHPMailer would also catch this, but + // the intent belongs here rather than depending on a downstream library). + // The `%0a`/`%0d` literals mirror the header-injection strip at the top of + // this method: a value can arrive percent-encoded from a source that was + // not URL-decoded, and stripping both forms is cheaper than proving no + // such path exists. The trade-off — a subject legitimately containing the + // text "%0a" loses it — is negligible against a header-injection strip. + $newlines = array( "\r", "\n", '%0a', '%0d' ); + $subject_tags = array_map( + static function ( $value ) use ( $newlines ) { + return str_replace( $newlines, ' ', $value ); + }, + $raw_values + ); + $subject_tags['{all_fields}'] = str_replace( $newlines, ' ', implode( '; ', $all_fields_text ) ); + + // Default email body if empty. + if ( empty( $email_body ) ) { + $email_body = __( "New form submission:\n\n{all_fields}\n\nSubmitted from: {page_url}", 'airo-wp' ); + } else { + $email_body = sanitize_textarea_field( $email_body ); + } + + // Replace merge tags — each context with its own map (see above). + $email_subject = str_replace( array_keys( $subject_tags ), array_values( $subject_tags ), $email_subject ); + $email_body = str_replace( array_keys( $body_tags ), array_values( $body_tags ), $email_body ); + + // Convert line breaks to
for HTML email. + $email_body = nl2br( $email_body ); + + // Set up headers. + $headers = array( + 'Content-Type: text/html; charset=UTF-8', + sprintf( 'From: %s <%s>', $email_from_name, $email_from ), + ); + + // Add Reply-To if specified and field exists. + if ( ! empty( $email_reply_to ) && isset( $fields[ $email_reply_to ] ) ) { + // Flatten through the shared helper: a multi-value field yields an + // array, and handing that to str_replace()/sanitize_email() below is a + // PHP 8 TypeError (strlen() on an array) that would take down the whole + // notification. self::stringify_field_value() guarantees a string. + $reply_to_value = self::stringify_field_value( $fields[ $email_reply_to ] ); + + // Strip newlines to prevent email header injection. + $reply_to_value = str_replace( array( "\r", "\n", '%0a', '%0d' ), '', $reply_to_value ); + $reply_to_value = sanitize_email( $reply_to_value ); + + if ( is_email( $reply_to_value ) ) { + $headers[] = sprintf( 'Reply-To: %s', $reply_to_value ); + } + } + + // Send email. + $email_sent = wp_mail( $email_to, $email_subject, $email_body, $headers ); + + // Store email delivery status in submission meta. + update_post_meta( $submission_id, '_dsg_email_sent', $email_sent ? 'yes' : 'no' ); + update_post_meta( $submission_id, '_dsg_email_to', $email_to ); + update_post_meta( $submission_id, '_dsg_email_sent_date', current_time( 'mysql' ) ); + + // Log email delivery if enabled in settings. + $form_settings = $this->get_form_settings(); + $enable_email_logging = isset( $form_settings['enable_email_logging'] ) ? $form_settings['enable_email_logging'] : false; + + if ( $enable_email_logging ) { + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional logging when enabled in settings. + sprintf( + 'airo-wp Form: Email %s for submission #%d (To: %s, Subject: %s)', + $email_sent ? 'sent successfully' : 'FAILED to send', + $submission_id, + $email_to, + $email_subject + ) + ); + } + + // Fire action hook for email monitoring/integration. + do_action( 'airowp_form_email_sent', $submission_id, $form_id, $email_sent, $email_to, $email_subject ); + } + + /** + * Clean up old form submissions based on retention settings. + * + * Called daily by cron job to delete submissions older than the configured retention period. + * Respects the retention_days setting (default: 30 days). + * + * Processes in batches to prevent timeout issues on sites with large numbers of submissions. + * Use 'airowp_cleanup_batch_size' filter to adjust batch size (default: 100). + * + * @since 1.2.0 + */ + public function cleanup_old_submissions() { + $form_settings = $this->get_form_settings(); + $retention_days = absint( $form_settings['retention_days'] ); + + // If retention is 0, keep submissions indefinitely (disable cleanup). + if ( 0 === $retention_days ) { + return; + } + + // Calculate cutoff date. + $cutoff_date = gmdate( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) ); + + // Batch size to prevent timeout (filterable). + $batch_size = apply_filters( 'airowp_cleanup_batch_size', 100 ); + + // Find old submissions (limited batch to prevent timeout). + $query = new WP_Query( + array( + 'post_type' => 'dsgo_form_submission', + 'post_status' => 'any', + 'date_query' => array( + array( + 'before' => $cutoff_date, + ), + ), + 'posts_per_page' => $batch_size, + 'fields' => 'ids', + 'no_found_rows' => true, + ) + ); + + $old_submissions = $query->posts; + + if ( empty( $old_submissions ) ) { + return; + } + + // Delete submissions and their metadata. + foreach ( $old_submissions as $submission_id ) { + wp_delete_post( $submission_id, true ); // Force delete (bypass trash). + } + + // Clear form submissions count cache. + delete_transient( 'dsgo_form_submissions_count' ); + + // Log cleanup for debugging. + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging. + sprintf( + 'airo-wp: Deleted %d form submissions older than %d days.', + count( $old_submissions ), + $retention_days + ) + ); + } + } + + /** + * Look up form block attributes from post content by form ID. + * + * Searches published posts for a form-builder block with the matching formId + * attribute. This ensures email configuration is read from the server-side + * block definition, not from client-submitted data. + * + * @param string $form_id Form identifier to look up. + * @return array|null Block attributes array, or null if not found. + */ + private function get_form_block_attributes( $form_id ) { + // Check transient cache first to avoid LIKE queries on every submission. + // v2 prefix invalidates older caches that were stored before block-type + // defaults were merged into parsed attributes. + $cache_key = 'airowp_form_attrs_v2_' . md5( $form_id ); + $cached = get_transient( $cache_key ); + + if ( false !== $cached ) { + return $cached; + } + + global $wpdb; + + // Note: esc_like() wraps the entire concatenated string including $form_id, + // so LIKE wildcards (%, _) in $form_id are escaped. prepare() handles SQL injection. + // LIMIT 5 accounts for edge cases like revisions or duplicate formIds. + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $posts = $wpdb->get_results( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + "SELECT ID, post_content FROM {$wpdb->posts} + WHERE post_content LIKE %s + AND post_content LIKE %s + AND post_status IN ('publish', 'private') + LIMIT 5", + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + '%' . $wpdb->esc_like( 'airo-wp/form-builder' ) . '%', + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + '%' . $wpdb->esc_like( '"formId":"' . $form_id . '"' ) . '%' + ) + ); + + if ( empty( $posts ) ) { + return null; + } + + foreach ( $posts as $post ) { + $blocks = parse_blocks( $post->post_content ); + $attrs = $this->find_form_block_attributes( $blocks, $form_id ); + if ( null !== $attrs ) { + // Cache for 1 hour. Invalidated on save_post via clear_form_attributes_cache(). + set_transient( $cache_key, $attrs, HOUR_IN_SECONDS ); + return $attrs; + } + } + + return null; + } + + /** + * Recursively search parsed blocks for a form-builder block with matching formId. + * + * @param array $blocks Parsed blocks array. + * @param string $form_id Form identifier to match. + * @return array|null Block attributes if found, null otherwise. + */ + private function find_form_block_attributes( $blocks, $form_id ) { + foreach ( $blocks as $block ) { + if ( + 'airo-wp/form-builder' === $block['blockName'] && + isset( $block['attrs']['formId'] ) && + $block['attrs']['formId'] === $form_id + ) { + // parse_blocks() returns only the attributes that were serialized into + // the block comment. The editor omits attributes that equal their + // declared default, so booleans like `enableEmail` (default true) and + // similar may be missing here. Merge in the block-type defaults so + // server-side consumers see the same attribute set the editor does. + return $this->apply_form_block_defaults( $block['attrs'] ); + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $result = $this->find_form_block_attributes( $block['innerBlocks'], $form_id ); + if ( null !== $result ) { + return $result; + } + } + } + + return null; + } + + /** + * Merge registered block-type attribute defaults into a parsed attributes array. + * + * @param array $attrs Parsed block attributes from parse_blocks(). + * @return array Attributes with block.json defaults filled in for missing keys. + */ + private function apply_form_block_defaults( $attrs ) { + if ( ! class_exists( '\WP_Block_Type_Registry' ) ) { + return $attrs; + } + + $block_type = \WP_Block_Type_Registry::get_instance()->get_registered( 'airo-wp/form-builder' ); + if ( ! $block_type || ! is_array( $block_type->attributes ) ) { + return $attrs; + } + + foreach ( $block_type->attributes as $key => $schema ) { + if ( array_key_exists( $key, $attrs ) ) { + continue; + } + if ( is_array( $schema ) && array_key_exists( 'default', $schema ) ) { + $attrs[ $key ] = $schema['default']; + } + } + + return $attrs; + } + + /** + * Look up server-defined field types for a form by form ID. + * + * Uses parsed block content so validation/sanitization does not rely on + * client-supplied field types. + * + * @param string $form_id Form identifier to look up. + * @return array Field types keyed by field name. + */ + private function get_form_field_types( $form_id ) { + $cache_key = 'airowp_form_field_types_' . md5( $form_id ); + $cached = get_transient( $cache_key ); + + if ( false !== $cached && is_array( $cached ) ) { + return $cached; + } + + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $posts = $wpdb->get_results( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + "SELECT ID, post_content FROM {$wpdb->posts} + WHERE post_content LIKE %s + AND post_content LIKE %s + AND post_status IN ('publish', 'private') + LIMIT 5", + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + '%' . $wpdb->esc_like( 'airo-wp/form-builder' ) . '%', + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + '%' . $wpdb->esc_like( '"formId":"' . $form_id . '"' ) . '%' + ) + ); + + if ( empty( $posts ) ) { + return array(); + } + + foreach ( $posts as $post ) { + $blocks = parse_blocks( $post->post_content ); + $field_types = $this->find_form_field_types( $blocks, $form_id ); + + if ( ! empty( $field_types ) ) { + set_transient( $cache_key, $field_types, HOUR_IN_SECONDS ); + return $field_types; + } + } + + return array(); + } + + /** + * Look up server-defined allowed values for constrained fields by form ID. + * + * Parallels get_form_field_types() but returns, per field name, the list of + * values the server will accept. Only select/checkbox/hidden fields are + * constrained; all other field types are omitted (unconstrained). Used to + * reject forged option values and hidden-field constants from the client. + * + * @param string $form_id Form identifier to look up. + * @return array Allowed values keyed by field name. + */ + private function get_form_field_value_constraints( $form_id ) { + $cache_key = 'airowp_form_field_constraints_' . md5( $form_id ); + $cached = get_transient( $cache_key ); + + if ( false !== $cached && is_array( $cached ) ) { + return $cached; + } + + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $posts = $wpdb->get_results( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + "SELECT ID, post_content FROM {$wpdb->posts} + WHERE post_content LIKE %s + AND post_content LIKE %s + AND post_status IN ('publish', 'private') + LIMIT 5", + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + '%' . $wpdb->esc_like( 'airo-wp/form-builder' ) . '%', + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + '%' . $wpdb->esc_like( '"formId":"' . $form_id . '"' ) . '%' + ) + ); + + if ( empty( $posts ) ) { + return array(); + } + + foreach ( $posts as $post ) { + $blocks = parse_blocks( $post->post_content ); + $constraints = $this->find_form_field_constraints( $blocks, $form_id ); + + if ( ! empty( $constraints ) ) { + set_transient( $cache_key, $constraints, HOUR_IN_SECONDS ); + return $constraints; + } + } + + return array(); + } + + /** + * Recursively search parsed blocks for a form-builder block and extract + * allowed values for constrained fields. + * + * @param array $blocks Parsed blocks array. + * @param string $form_id Form identifier to match. + * @return array Allowed values keyed by field name. + */ + private function find_form_field_constraints( $blocks, $form_id ) { + foreach ( $blocks as $block ) { + if ( + 'airo-wp/form-builder' === $block['blockName'] && + isset( $block['attrs']['formId'] ) && + $block['attrs']['formId'] === $form_id + ) { + return $this->extract_field_value_constraints_from_blocks( + isset( $block['innerBlocks'] ) ? $block['innerBlocks'] : array() + ); + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $result = $this->find_form_field_constraints( $block['innerBlocks'], $form_id ); + if ( ! empty( $result ) ) { + return $result; + } + } + } + + return array(); + } + + /** + * Extract allowed values for constrained fields from a form's inner blocks. + * + * - select: the `value` of each entry in the `options` attribute. + * - checkbox: the single `value` attribute (default "1"). + * - hidden: the server-defined `value` attribute (a constant). + * + * Fields without a constraint are not added to the map. + * + * @param array $blocks Parsed inner blocks. + * @return array Allowed values keyed by field name. + */ + private function extract_field_value_constraints_from_blocks( $blocks ) { + $constraints = array(); + + foreach ( $blocks as $block ) { + $block_name = isset( $block['blockName'] ) ? $block['blockName'] : ''; + $field_name = isset( $block['attrs']['fieldName'] ) ? sanitize_text_field( $block['attrs']['fieldName'] ) : ''; + $attrs = isset( $block['attrs'] ) ? $block['attrs'] : array(); + + if ( $field_name ) { + switch ( $block_name ) { + case 'airo-wp/form-select-field': + if ( isset( $attrs['options'] ) && is_array( $attrs['options'] ) ) { + $values = array(); + foreach ( $attrs['options'] as $option ) { + if ( isset( $option['value'] ) ) { + $values[] = (string) $option['value']; + } + } + $constraints[ $field_name ] = $values; + } + break; + + case 'airo-wp/form-checkbox-field': + $constraints[ $field_name ] = array( + isset( $attrs['value'] ) ? (string) $attrs['value'] : '1', + ); + break; + + case 'airo-wp/form-hidden-field': + $constraints[ $field_name ] = array( + isset( $attrs['value'] ) ? (string) $attrs['value'] : '', + ); + break; + } + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $constraints = array_merge( + $constraints, + $this->extract_field_value_constraints_from_blocks( $block['innerBlocks'] ) + ); + } + } + + return $constraints; + } + + /** + * Recursively search parsed blocks for a form-builder block and extract field types. + * + * @param array $blocks Parsed blocks array. + * @param string $form_id Form identifier to match. + * @return array Field types keyed by field name. + */ + private function find_form_field_types( $blocks, $form_id ) { + foreach ( $blocks as $block ) { + if ( + 'airo-wp/form-builder' === $block['blockName'] && + isset( $block['attrs']['formId'] ) && + $block['attrs']['formId'] === $form_id + ) { + return $this->extract_field_types_from_blocks( + isset( $block['innerBlocks'] ) ? $block['innerBlocks'] : array() + ); + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $result = $this->find_form_field_types( $block['innerBlocks'], $form_id ); + if ( ! empty( $result ) ) { + return $result; + } + } + } + + return array(); + } + + /** + * Extract field types from a form block's inner blocks. + * + * @param array $blocks Parsed inner blocks. + * @return array Field types keyed by field name. + */ + private function extract_field_types_from_blocks( $blocks ) { + $field_types = array(); + + foreach ( $blocks as $block ) { + $block_name = isset( $block['blockName'] ) ? $block['blockName'] : ''; + $field_name = isset( $block['attrs']['fieldName'] ) ? sanitize_text_field( $block['attrs']['fieldName'] ) : ''; + $field_type = $this->map_block_name_to_field_type( $block_name ); + + if ( $field_name && $field_type ) { + $field_types[ $field_name ] = $field_type; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $field_types = array_merge( + $field_types, + $this->extract_field_types_from_blocks( $block['innerBlocks'] ) + ); + } + } + + return $field_types; + } + + /** + * Map form field block names to server-side field types. + * + * @param string $block_name Block name. + * @return string|null Server-side field type, or null when unsupported. + */ + private function map_block_name_to_field_type( $block_name ) { + switch ( $block_name ) { + case 'airo-wp/form-text-field': + return 'text'; + + case 'airo-wp/form-email-field': + return 'email'; + + case 'airo-wp/form-textarea-field': + return 'textarea'; + + case 'airo-wp/form-number-field': + return 'number'; + + case 'airo-wp/form-phone-field': + return 'tel'; + + case 'airo-wp/form-url-field': + return 'url'; + + case 'airo-wp/form-date-field': + return 'date'; + + case 'airo-wp/form-time-field': + return 'time'; + + case 'airo-wp/form-select-field': + return 'select'; + + case 'airo-wp/form-checkbox-field': + return 'checkbox'; + + case 'airo-wp/form-hidden-field': + return 'hidden'; + + default: + return null; + } + } + + /** + * Clear cached form block attributes when a post is saved. + * + * Hooked to save_post to ensure email config changes take effect immediately. + * + * @param int $post_id Post ID being saved. + */ + public function clear_form_attributes_cache( $post_id ) { + $post = get_post( $post_id ); + if ( ! $post || false === strpos( $post->post_content, 'airo-wp/form-builder' ) ) { + return; + } + + $blocks = parse_blocks( $post->post_content ); + $this->invalidate_form_block_transients( $blocks ); + } + + /** + * Recursively delete cached transients for form blocks. + * + * @param array $blocks Parsed blocks array. + */ + private function invalidate_form_block_transients( $blocks ) { + foreach ( $blocks as $block ) { + if ( + 'airo-wp/form-builder' === $block['blockName'] && + isset( $block['attrs']['formId'] ) + ) { + delete_transient( 'airowp_form_attrs_v2_' . md5( $block['attrs']['formId'] ) ); + delete_transient( 'airowp_form_field_types_' . md5( $block['attrs']['formId'] ) ); + delete_transient( 'airowp_form_field_constraints_' . md5( $block['attrs']['formId'] ) ); + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $this->invalidate_form_block_transients( $block['innerBlocks'] ); + } + } + } +} diff --git a/includes/Blocks/Common/Forms/FormSecurity.php b/includes/Blocks/Common/Forms/FormSecurity.php new file mode 100644 index 0000000..ba7d9d5 --- /dev/null +++ b/includes/Blocks/Common/Forms/FormSecurity.php @@ -0,0 +1,337 @@ +get_client_ip() ); + + return new WP_Error( + 'spam_detected', + __( 'Spam submission rejected.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Check time-based spam detection. + * + * @param string $timestamp Submission timestamp. + * @param string $form_id Form identifier. + * @return true|WP_Error True if valid, WP_Error if too fast. + */ + public function check_submission_timing( string $timestamp, string $form_id ) { + if ( empty( $timestamp ) ) { + return true; + } + + $elapsed = ( time() * 1000 ) - intval( $timestamp ); + if ( $elapsed < 3000 ) { + do_action( 'airowp_form_spam_detected', $form_id, 'too_fast', $this->get_client_ip(), array( 'elapsed_ms' => $elapsed ) ); + + return new WP_Error( + 'too_fast', + __( 'Submission too fast. Please try again.', 'airo-wp' ), + array( 'status' => 429 ) + ); + } + + return true; + } + + /** + * Check rate limiting for form submissions. + * + * @param string $form_id Form ID. + * @param int $block_max Max submissions from block attributes. Default 3. + * @return true|WP_Error True if allowed, WP_Error if rate limited. + */ + public function check_rate_limit( string $form_id, int $block_max = 3 ) { + $ip_address = $this->get_client_ip(); + $key = 'form_submit_' . $form_id . '_' . md5( $ip_address ); + $count = get_transient( $key ); + + $max_submissions = apply_filters( 'airowp_form_rate_limit_count', $block_max, $form_id ); + + if ( false !== $count && $count >= $max_submissions ) { + do_action( 'airowp_form_rate_limit_exceeded', $form_id, $ip_address, $count, $max_submissions ); + + return new WP_Error( + 'rate_limit', + __( 'Too many submissions. Please try again later.', 'airo-wp' ), + array( 'status' => 429 ) + ); + } + + return true; + } + + /** + * Increment rate limit counter after successful submission. + * + * @param string $form_id Form ID. + * @param int $block_window Time window in seconds from block attributes. Default 60. + */ + public function increment_rate_limit( string $form_id, int $block_window = 60 ): void { + $ip_address = $this->get_client_ip(); + $key = 'form_submit_' . $form_id . '_' . md5( $ip_address ); + $count = get_transient( $key ); + + $time_window = apply_filters( 'airowp_form_rate_limit_window', $block_window, $form_id ); + + if ( false === $count ) { + set_transient( $key, 1, $time_window ); + } else { + set_transient( $key, $count + 1, $time_window ); + } + } + + /** + * Verify Cloudflare Turnstile token. + * + * @param string $token The Turnstile response token from the frontend. + * @return true|WP_Error True on success, WP_Error on verification failure. + */ + public function verify_turnstile( string $token ) { + $settings = get_option( 'airowp_settings', array() ); + $secret_key = isset( $settings['integrations']['turnstile_secret_key'] ) + ? $settings['integrations']['turnstile_secret_key'] + : ''; + + // If no secret key configured, skip verification (graceful degradation). + if ( empty( $secret_key ) ) { + return true; + } + + // 3s timeout is intentional. Turnstile runs on Cloudflare's edge network and + // should respond in well under a second; this verification call blocks the + // form submission response, so a generous timeout directly penalises customer + // experience. On timeout, wp_remote_post() returns a WP_Error and we degrade + // gracefully (let the submission through) rather than punish the user. + $response = wp_remote_post( + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', + array( + 'timeout' => 3, + 'body' => array( + 'secret' => $secret_key, + 'response' => $token, + 'remoteip' => $this->get_client_ip(), + ), + ) + ); + + // Handle HTTP errors (graceful degradation). + if ( is_wp_error( $response ) ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( 'airo-wp Turnstile: HTTP error - ' . $response->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + return true; + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + if ( ! is_array( $data ) ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( 'airo-wp Turnstile: Invalid response from Cloudflare' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + return true; + } + + if ( ! isset( $data['success'] ) || true !== $data['success'] ) { + $error_codes = isset( $data['error-codes'] ) ? implode( ', ', $data['error-codes'] ) : 'unknown'; + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( 'airo-wp Turnstile: Verification failed - ' . $error_codes ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + + return new WP_Error( + 'turnstile_failed', + __( 'Security verification failed. Please try again.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Get client IP address with trusted proxy support. + * + * @return string IP address. + */ + public function get_client_ip(): string { + // REMOTE_ADDR is set by the web server from the TCP peer. Validate it as a + // real IP address so we never return a spoofed or malformed value. + $remote_addr = $this->read_valid_ip( 'REMOTE_ADDR' ); + + if ( null === $remote_addr ) { + return 'unknown'; + } + + $trusted_proxies = apply_filters( 'airowp_trusted_proxies', array() ); + + // Forwarded headers are only trustworthy when the connecting host is a + // known proxy; otherwise a client could set them to spoof its address. + if ( empty( $trusted_proxies ) || ! is_array( $trusted_proxies ) + || ! $this->is_trusted_proxy( $remote_addr, $trusted_proxies ) ) { + return $remote_addr; + } + + // Ordered most-specific-first. The first header yielding a public, + // routable address wins; comma-separated chains are walked left to right. + $proxy_headers = array( + 'HTTP_CF_CONNECTING_IP', + 'HTTP_X_REAL_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_CLIENT_IP', + ); + + foreach ( $proxy_headers as $header ) { + $ip = $this->read_forwarded_ip( $header ); + + if ( null !== $ip ) { + return $ip; + } + } + + return $remote_addr; + } + + /** + * Read a $_SERVER key and return it only if it is a valid IP address. + * + * Accepts both IPv4 and IPv6. Returns null when the key is absent, empty, + * or not a syntactically valid IP. + * + * @param string $key $_SERVER key to read (e.g. 'REMOTE_ADDR'). + * @return string|null Validated IP address, or null. + */ + private function read_valid_ip( string $key ): ?string { + if ( empty( $_SERVER[ $key ] ) ) { + return null; + } + + // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders -- Value is validated with filter_var( FILTER_VALIDATE_IP ) before it is returned or used. + $value = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); + + return filter_var( $value, FILTER_VALIDATE_IP ) ? $value : null; + } + + /** + * Extract the first public, routable IP from a forwarded proxy header. + * + * Handles single values and comma-separated chains (X-Forwarded-For style), + * skipping private and reserved ranges so we return the real client address. + * + * @param string $key $_SERVER key to read (e.g. 'HTTP_X_FORWARDED_FOR'). + * @return string|null First valid public IP, or null. + */ + private function read_forwarded_ip( string $key ): ?string { + if ( empty( $_SERVER[ $key ] ) ) { + return null; + } + + // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders -- Each candidate is validated with filter_var( FILTER_VALIDATE_IP ) below before use. + $raw = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); + + foreach ( explode( ',', $raw ) as $candidate ) { + $candidate = trim( $candidate ); + + if ( filter_var( $candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { + return $candidate; + } + } + + return null; + } + + /** + * Check if IP address is in trusted proxy list (supports CIDR notation). + * + * @param string $ip IP address to check. + * @param array $trusted_proxies List of trusted IPs/CIDR ranges. + * @return bool True if trusted, false otherwise. + */ + private function is_trusted_proxy( string $ip, array $trusted_proxies ): bool { + foreach ( $trusted_proxies as $trusted ) { + if ( strpos( $trusted, '/' ) !== false ) { + if ( $this->ip_in_range( $ip, $trusted ) ) { + return true; + } + } elseif ( $ip === $trusted ) { + return true; + } + } + return false; + } + + /** + * Check if IP is in CIDR range. + * + * @param string $ip IP address to check. + * @param string $cidr CIDR notation (e.g., '192.168.1.0/24'). + * @return bool True if IP is in range. + */ + private function ip_in_range( string $ip, string $cidr ): bool { + $parts = explode( '/', $cidr ); + if ( count( $parts ) !== 2 ) { + return false; + } + list( $subnet, $mask ) = $parts; + + $ip_long = ip2long( $ip ); + $subnet_long = ip2long( $subnet ); + + if ( false === $ip_long || false === $subnet_long ) { + return false; + } + + $mask = (int) $mask; + if ( $mask < 0 || $mask > 32 ) { + return false; + } + if ( 0 === $mask ) { + return true; + } + + $mask_long = -1 << ( 32 - $mask ); + + return ( $ip_long & $mask_long ) === ( $subnet_long & $mask_long ); + } +} diff --git a/includes/Blocks/Common/Forms/FormSubmissions.php b/includes/Blocks/Common/Forms/FormSubmissions.php new file mode 100644 index 0000000..212f360 --- /dev/null +++ b/includes/Blocks/Common/Forms/FormSubmissions.php @@ -0,0 +1,290 @@ + _x( 'Form Submissions', 'post type general name', 'airo-wp' ), + 'singular_name' => _x( 'Form Submission', 'post type singular name', 'airo-wp' ), + 'menu_name' => _x( 'Form Submissions', 'admin menu', 'airo-wp' ), + 'name_admin_bar' => _x( 'Form Submission', 'add new on admin bar', 'airo-wp' ), + 'all_items' => __( 'All Submissions', 'airo-wp' ), + 'view_item' => __( 'View Submission', 'airo-wp' ), + 'search_items' => __( 'Search Submissions', 'airo-wp' ), + 'not_found' => __( 'No submissions found', 'airo-wp' ), + 'not_found_in_trash' => __( 'No submissions found in Trash', 'airo-wp' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => true, + 'show_in_menu' => 'airo-wp', // Show under airo-wp menu. + 'query_var' => true, + 'rewrite' => false, + 'show_in_rest' => false, // Prevent REST API access. + 'capability_type' => 'dsgo_form_submission', + 'capabilities' => array( + 'edit_post' => 'manage_options', + 'read_post' => 'manage_options', + 'delete_post' => 'manage_options', + 'edit_posts' => 'manage_options', + 'edit_others_posts' => 'manage_options', + 'edit_private_posts' => 'manage_options', + 'edit_published_posts' => 'manage_options', + 'delete_posts' => 'manage_options', + 'delete_others_posts' => 'manage_options', + 'delete_private_posts' => 'manage_options', + 'delete_published_posts' => 'manage_options', + 'publish_posts' => 'manage_options', + 'read_private_posts' => 'manage_options', + 'create_posts' => 'do_not_allow', // Prevent manual creation - only via form submission. + ), + 'map_meta_cap' => false, // Disabled - all capabilities explicitly mapped to manage_options. + 'has_archive' => false, + 'hierarchical' => false, + 'supports' => array( 'title' ), + ); + + register_post_type( 'dsgo_form_submission', $args ); + } + + /** + * Add meta boxes to submission edit screen. + */ + public function add_meta_boxes() { + add_meta_box( + 'dsg_submission_details', + __( 'Submission Details', 'airo-wp' ), + array( $this, 'render_submission_details' ), + 'dsgo_form_submission', + 'normal', + 'high' + ); + + add_meta_box( + 'dsg_submission_meta', + __( 'Submission Information', 'airo-wp' ), + array( $this, 'render_submission_meta' ), + 'dsgo_form_submission', + 'side', + 'default' + ); + } + + /** + * Render submission details meta box. + * + * @param \WP_Post $post Post object. + */ + public function render_submission_details( $post ) { + $fields = get_post_meta( $post->ID, '_dsg_form_fields', true ); + + if ( empty( $fields ) || ! is_array( $fields ) ) { + echo '

' . esc_html__( 'No form data available.', 'airo-wp' ) . '

'; + return; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ( $fields as $field_name => $field_data ) { + $value = isset( $field_data['value'] ) ? $field_data['value'] : ''; + $type = isset( $field_data['type'] ) ? $field_data['type'] : 'text'; + + // Format value based on type. + if ( 'email' === $type ) { + $value = '' . esc_html( $value ) . ''; + } elseif ( 'url' === $type ) { + $value = '' . esc_html( $value ) . ''; + } elseif ( 'textarea' === $type ) { + $value = '
' . esc_html( $value ) . '
'; + } else { + $value = esc_html( $value ); + } + + echo ''; + echo ''; + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo ''; + echo ''; + } + + echo ''; + echo '
' . esc_html__( 'Field Name', 'airo-wp' ) . '' . esc_html__( 'Value', 'airo-wp' ) . '' . esc_html__( 'Type', 'airo-wp' ) . '
' . esc_html( $field_name ) . '' . $value . '' . esc_html( $type ) . '
'; + } + + /** + * Render submission meta information. + * + * @param \WP_Post $post Post object. + */ + public function render_submission_meta( $post ) { + $form_id = get_post_meta( $post->ID, '_dsg_form_id', true ); + $ip_address = get_post_meta( $post->ID, '_dsg_submission_ip', true ); + $user_agent = get_post_meta( $post->ID, '_dsg_submission_user_agent', true ); + $referer = get_post_meta( $post->ID, '_dsg_submission_referer', true ); + $date = get_post_meta( $post->ID, '_dsg_submission_date', true ); + $email_sent = get_post_meta( $post->ID, '_dsg_email_sent', true ); + $email_to = get_post_meta( $post->ID, '_dsg_email_to', true ); + $email_sent_date = get_post_meta( $post->ID, '_dsg_email_sent_date', true ); + + echo '
'; + echo '' . esc_html__( 'Form ID:', 'airo-wp' ) . '
'; + echo '' . esc_html( $form_id ) . ''; + echo '
'; + + if ( $date ) { + echo '
'; + echo '' . esc_html__( 'Submitted:', 'airo-wp' ) . '
'; + echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $date ) ) ); + echo '
'; + } + + // Email delivery status. + if ( '' !== $email_sent ) { + $is_sent = ( 'yes' === $email_sent ); + $status_class = $is_sent ? 'airo-wp-submission-status--success' : 'airo-wp-submission-status--error'; + + printf( '
', esc_attr( $status_class ) ); + echo '' . esc_html__( 'Email Status:', 'airo-wp' ) . '
'; + if ( $is_sent ) { + echo '✓ ' . esc_html__( 'Sent Successfully', 'airo-wp' ) . ''; + if ( $email_to ) { + echo '
' . esc_html__( 'To:', 'airo-wp' ) . ' ' . esc_html( $email_to ) . ''; + } + if ( $email_sent_date ) { + echo '
' . esc_html__( 'Sent:', 'airo-wp' ) . ' ' . esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $email_sent_date ) ) . ''; + } + } else { + echo '✗ ' . esc_html__( 'Failed to Send', 'airo-wp' ) . ''; + if ( $email_to ) { + echo '
' . esc_html__( 'Attempted to:', 'airo-wp' ) . ' ' . esc_html( $email_to ) . ''; + } + } + echo '
'; + } + + if ( $ip_address ) { + echo '
'; + echo '' . esc_html__( 'IP Address:', 'airo-wp' ) . '
'; + echo '' . esc_html( $ip_address ) . ''; + echo '
'; + } + + if ( $referer ) { + echo '
'; + echo '' . esc_html__( 'Referrer:', 'airo-wp' ) . '
'; + echo '' . esc_html( $referer ) . ''; + echo '
'; + } + + if ( $user_agent ) { + echo '
'; + echo '' . esc_html__( 'User Agent:', 'airo-wp' ) . '
'; + echo '' . esc_html( $user_agent ) . ''; + echo '
'; + } + } + + /** + * Customize admin columns. + * + * @param array $columns Existing columns. + * @return array Modified columns. + */ + public function custom_columns( $columns ) { + $new_columns = array( + 'cb' => $columns['cb'], + 'title' => __( 'Submission', 'airo-wp' ), + 'form_id' => __( 'Form ID', 'airo-wp' ), + 'email_status' => __( 'Email', 'airo-wp' ), + 'ip_address' => __( 'IP Address', 'airo-wp' ), + 'date' => __( 'Date', 'airo-wp' ), + ); + + return $new_columns; + } + + /** + * Render custom column content. + * + * @param string $column Column name. + * @param int $post_id Post ID. + */ + public function custom_column_content( $column, $post_id ) { + switch ( $column ) { + case 'form_id': + $form_id = get_post_meta( $post_id, '_dsg_form_id', true ); + echo $form_id ? '' . esc_html( $form_id ) . '' : '—'; + break; + + case 'email_status': + $email_sent = get_post_meta( $post_id, '_dsg_email_sent', true ); + if ( '' === $email_sent ) { + echo ''; + } elseif ( 'yes' === $email_sent ) { + echo '✓ ' . esc_html__( 'Sent', 'airo-wp' ) . ''; + } else { + echo '✗ ' . esc_html__( 'Failed', 'airo-wp' ) . ''; + } + break; + + case 'ip_address': + $ip_address = get_post_meta( $post_id, '_dsg_submission_ip', true ); + echo $ip_address ? '' . esc_html( $ip_address ) . '' : '—'; + break; + } + } + + /** + * Enqueue admin styles for the submission meta box. + * + * @param string $hook Current admin page hook suffix. + */ + public function enqueue_submission_styles( string $hook ) : void { + if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) { + return; + } + wp_register_style( 'airowp-submissions', false, array(), AIRO_WP_VERSION ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion + wp_enqueue_style( 'airowp-submissions' ); + wp_add_inline_style( 'airowp-submissions', '.airo-wp-submission-status{margin-bottom:1em;padding:10px;border-left:3px solid}.airo-wp-submission-status--success{background:#d4edda;border-color:#28a745}.airo-wp-submission-status--success .airo-wp-submission-status__label{color:#155724}.airo-wp-submission-status--error{background:#f8d7da;border-color:#dc3545}.airo-wp-submission-status--error .airo-wp-submission-status__label{color:#721c24}' ); + } +} diff --git a/includes/Blocks/Common/Modal/ModalHooks.php b/includes/Blocks/Common/Modal/ModalHooks.php new file mode 100644 index 0000000..95e2e60 --- /dev/null +++ b/includes/Blocks/Common/Modal/ModalHooks.php @@ -0,0 +1,327 @@ +sanitize_attributes( $attributes ); + + /** + * Filter modal CSS classes. + * + * @param array $classes Array of CSS classes. + * @param array $attributes Modal block attributes. + * @param array $block Complete block data. + * @return array Modified classes. + */ + $classes = apply_filters( + 'airowp_modal_classes', + array( 'airo-wp-modal' ), + $attributes, + $block + ); + + // Sanitize classes after filtering. + $classes = array_map( 'sanitize_html_class', array_filter( $classes ) ); + + /** + * Filter modal data attributes. + * + * These attributes are read by the JavaScript modal controller. + * + * @param array $data_attrs Associative array of data attributes. + * @param array $attributes Modal block attributes. + * @param array $block Complete block data. + * @return array Modified data attributes. + */ + $data_attrs = apply_filters( + 'airowp_modal_data_attributes', + $this->get_default_data_attributes( $attributes ), + $attributes, + $block + ); + + // Sanitize data attributes after filtering. + $data_attrs = $this->sanitize_data_attributes( $data_attrs ); + + /** + * Action fired before modal block renders. + * + * @param array $attributes Modal block attributes. + * @param array $block Complete block data. + */ + do_action( 'airowp_modal_before_render', $attributes, $block ); + + /** + * Filter complete modal block content. + * + * This is the final filter before output. Use this for major structural changes. + * + * @param string $block_content Current block HTML. + * @param array $attributes Modal block attributes. + * @param array $block Complete block data. + * @param array $classes Filtered CSS classes. + * @param array $data_attrs Filtered data attributes. + * @return string Modified block content. + */ + $block_content = apply_filters( + 'airowp_modal_content', + $block_content, + $attributes, + $block, + $classes, + $data_attrs + ); + + /** + * Action fired after modal block renders. + * + * @param string $block_content Rendered block HTML. + * @param array $attributes Modal block attributes. + * @param array $block Complete block data. + */ + do_action( 'airowp_modal_after_render', $block_content, $attributes, $block ); + + return $block_content; + } + + /** + * Get default data attributes from modal attributes. + * + * @param array $attributes Modal block attributes. + * @return array Data attributes array. + */ + private function get_default_data_attributes( $attributes ) { + $data_attrs = array(); + + // Map modal attributes to data attributes. + $attr_map = array( + 'modalId' => 'modal-id', + 'animationType' => 'animation-type', + 'animationDuration' => 'animation-duration', + 'closeOnBackdrop' => 'close-on-backdrop', + 'closeOnEsc' => 'close-on-esc', + 'disableBodyScroll' => 'disable-body-scroll', + 'allowHashTrigger' => 'allow-hash-trigger', + 'updateUrlOnOpen' => 'update-url-on-open', + 'autoTriggerType' => 'auto-trigger-type', + 'autoTriggerDelay' => 'auto-trigger-delay', + 'autoTriggerFrequency' => 'auto-trigger-frequency', + 'cookieDuration' => 'cookie-duration', + 'exitIntentSensitivity' => 'exit-intent-sensitivity', + 'exitIntentMinTime' => 'exit-intent-min-time', + 'exitIntentExcludeMobile' => 'exit-intent-exclude-mobile', + 'scrollDepth' => 'scroll-depth', + 'scrollDirection' => 'scroll-direction', + 'timeOnPage' => 'time-on-page', + 'galleryGroupId' => 'gallery-group-id', + 'galleryIndex' => 'gallery-index', + 'showGalleryNavigation' => 'show-gallery-navigation', + 'navigationStyle' => 'navigation-style', + 'navigationPosition' => 'navigation-position', + ); + + foreach ( $attr_map as $attr_key => $data_key ) { + if ( isset( $attributes[ $attr_key ] ) ) { + $value = $attributes[ $attr_key ]; + + // Convert booleans to strings. + if ( is_bool( $value ) ) { + $value = $value ? 'true' : 'false'; + } + + $data_attrs[ $data_key ] = $value; + } + } + + return $data_attrs; + } + + /** + * Sanitize modal attributes. + * + * @param array $attributes Attributes to sanitize. + * @return array Sanitized attributes. + */ + private function sanitize_attributes( $attributes ) { + $sanitized = array(); + + // Define expected attribute types and validation. + $validators = array( + 'modalId' => 'sanitize_key', + 'width' => 'sanitize_text_field', + 'maxWidth' => 'sanitize_text_field', + 'height' => 'sanitize_text_field', + 'maxHeight' => 'sanitize_text_field', + 'animationType' => array( $this, 'validate_enum' ), + 'animationDuration' => 'absint', + 'overlayOpacity' => 'absint', + 'overlayColor' => 'sanitize_hex_color', + 'overlayBlur' => 'absint', + 'closeOnBackdrop' => 'rest_sanitize_boolean', + 'closeOnEsc' => 'rest_sanitize_boolean', + 'showCloseButton' => 'rest_sanitize_boolean', + 'closeButtonPosition' => array( $this, 'validate_enum' ), + 'closeButtonSize' => 'absint', + 'closeButtonIconColor' => 'sanitize_text_field', + 'closeButtonBgColor' => 'sanitize_text_field', + 'disableBodyScroll' => 'rest_sanitize_boolean', + 'allowHashTrigger' => 'rest_sanitize_boolean', + 'updateUrlOnOpen' => 'rest_sanitize_boolean', + 'autoTriggerType' => array( $this, 'validate_enum' ), + 'autoTriggerDelay' => 'absint', + 'autoTriggerFrequency' => array( $this, 'validate_enum' ), + 'cookieDuration' => 'absint', + 'exitIntentSensitivity' => array( $this, 'validate_enum' ), + 'exitIntentMinTime' => 'absint', + 'exitIntentExcludeMobile' => 'rest_sanitize_boolean', + 'scrollDepth' => 'absint', + 'scrollDirection' => array( $this, 'validate_enum' ), + 'timeOnPage' => 'absint', + 'galleryGroupId' => 'sanitize_key', + 'galleryIndex' => 'absint', + 'showGalleryNavigation' => 'rest_sanitize_boolean', + 'navigationStyle' => array( $this, 'validate_enum' ), + 'navigationPosition' => array( $this, 'validate_enum' ), + ); + + foreach ( $attributes as $key => $value ) { + if ( isset( $validators[ $key ] ) ) { + $sanitized[ $key ] = call_user_func( $validators[ $key ], $value, $key ); + } else { + // If no validator defined for attribute, sanitize as text. + $sanitized[ $key ] = sanitize_text_field( $value ); + } + } + + return $sanitized; + } + + /** + * Validate enum values. + * + * @param mixed $value Value to validate. + * @param string $key Attribute key. + * @return mixed Validated value or default. + */ + private function validate_enum( $value, $key ) { + // Define valid enum values for each attribute. + $enums = array( + 'animationType' => array( 'fade', 'slide-up', 'slide-down', 'zoom', 'none' ), + 'closeButtonPosition' => array( 'inside-top-right', 'inside-top-left', 'top-right', 'top-left' ), + 'autoTriggerType' => array( 'none', 'pageLoad', 'exitIntent', 'scroll', 'time' ), + 'autoTriggerFrequency' => array( 'always', 'session', 'once' ), + 'exitIntentSensitivity' => array( 'low', 'medium', 'high' ), + 'scrollDirection' => array( 'down', 'both' ), + 'navigationStyle' => array( 'arrows', 'chevrons', 'text' ), + 'navigationPosition' => array( 'sides', 'bottom', 'top' ), + ); + + if ( isset( $enums[ $key ] ) && in_array( $value, $enums[ $key ], true ) ) { + return $value; + } + + // Return first valid value as default if invalid value provided. + return isset( $enums[ $key ] ) ? $enums[ $key ][0] : sanitize_text_field( $value ); + } + + /** + * Sanitize data attributes. + * + * @param array $data_attrs Data attributes to sanitize. + * @return array Sanitized data attributes. + */ + private function sanitize_data_attributes( $data_attrs ) { + $sanitized = array(); + + foreach ( $data_attrs as $key => $value ) { + // Sanitize key. + $clean_key = sanitize_key( $key ); + + // Sanitize value - escape for HTML attribute context. + if ( is_numeric( $value ) ) { + $clean_value = (string) $value; + } elseif ( is_string( $value ) ) { + $clean_value = esc_attr( $value ); + } else { + // Convert other types to string and escape. + $clean_value = esc_attr( (string) $value ); + } + + $sanitized[ $clean_key ] = $clean_value; + } + + return $sanitized; + } + + /** + * Add custom scripts to footer for modal integrations. + * + * Developers can use this action to inject custom JavaScript for modal events. + */ + public function modal_footer_scripts() { + /** + * Action fired in footer for custom modal scripts. + * + * Use this to add custom JavaScript that interacts with the modal API. + * + * Example: + * ```php + * add_action('airowp_modal_footer_scripts', function() { + * ?> + * + * \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'handle_render' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'queryId' => array( + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ), + + // NOTE: `attributes` and `params` are nested objects; WP only enforces the + // top-level type. The shared render helper (airowp_query_render) is + // responsible for per-field sanitization of every value before it reaches + // WP_Query args or HTML output. Do NOT assume these arrive sanitized. + 'attributes' => array( + 'type' => 'object', + 'required' => true, + ), + 'page' => array( + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + ), + 'innerBlocks' => array( + 'type' => 'string', + 'default' => '', + ), + 'params' => array( + 'type' => 'object', + 'default' => array(), + ), + 'currentUrl' => array( + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'esc_url_raw', + ), + ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/query/filter-register', + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'handle_filter_register' ), + 'permission_callback' => array( $this, 'check_manage_options_permission' ), + 'args' => array( + 'filter_key' => array( + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ), + 'config' => array( + 'type' => 'object', + 'required' => true, + ), + ), + ) + ); + + // Admin-only routes (manage_options). + + register_rest_route( + self::REST_NAMESPACE, + '/query/filter-status', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'handle_filter_status' ), + 'permission_callback' => array( $this, 'check_manage_options_permission' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/query/filter-rebuild', + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'handle_filter_rebuild' ), + 'permission_callback' => array( $this, 'check_manage_options_permission' ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/query/preview', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'handle_preview' ), + 'permission_callback' => array( $this, 'check_edit_posts_permission' ), + 'args' => array( + 'attributes' => array( + 'type' => 'object', + 'required' => true, + ), + ), + ) + ); + + register_rest_route( + self::REST_NAMESPACE, + '/query/filters', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'handle_filters_list' ), + 'permission_callback' => array( $this, 'check_manage_options_permission' ), + ), + array( + 'methods' => \WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'handle_filter_unregister' ), + 'permission_callback' => array( $this, 'check_manage_options_permission' ), + 'args' => array( + 'filter_key' => array( + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ), + ), + ), + ) + ); + } + + /** + * Checks that the request carries a valid nonce and the user has manage_options. + * + * Used by every admin-only filter route: /filter-register, /filter-status, + * /filter-rebuild, /filters (list), /filters/{key} (delete). Editor-level + * users go through check_edit_posts_permission instead (used only by the + * /query/preview route). + * + * @param \WP_REST_Request $request The REST request. + * @return true|\WP_Error + */ + public function check_manage_options_permission( \WP_REST_Request $request ) { + if ( ! is_user_logged_in() ) { + return new \WP_Error( + 'rest_forbidden', + __( 'You must be logged in.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + $nonce = $request->get_header( 'X-WP-Nonce' ); + if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { + return new \WP_Error( + 'rest_forbidden', + __( 'Invalid nonce.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + if ( ! current_user_can( 'manage_options' ) ) { + return new \WP_Error( + 'rest_forbidden', + __( 'Insufficient permissions.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Handles the preview REST request. + * + * For `source === 'posts'` this returns a WP_Error (client uses useEntityRecords). + * For `source === 'users'` runs WP_User_Query limited to perPage results. + * For `source === 'terms'` runs get_terms() limited to perPage results. + * + * @param \WP_REST_Request $request The REST request. + * @return \WP_REST_Response|\WP_Error + */ + public function handle_preview( \WP_REST_Request $request ) { + $attributes = (array) $request->get_param( 'attributes' ); + $source = isset( $attributes['source'] ) ? sanitize_key( $attributes['source'] ) : 'posts'; + $per_page = isset( $attributes['perPage'] ) ? max( 1, min( 100, (int) $attributes['perPage'] ) ) : 6; + + if ( 'posts' === $source ) { + return new \WP_Error( + 'not_needed', + __( 'Use useEntityRecords for posts source preview.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + if ( 'users' === $source ) { + $user_query = new \WP_User_Query( + array( + 'number' => $per_page, + 'orderby' => 'registered', + 'order' => 'DESC', + 'fields' => array( 'ID', 'display_name' ), + ) + ); + $items = array(); + foreach ( $user_query->get_results() as $user ) { + $items[] = array( + 'id' => (int) $user->ID, + 'name' => (string) $user->display_name, + 'type' => 'user', + ); + } + return rest_ensure_response( $items ); + } + + if ( 'terms' === $source ) { + $taxonomy = isset( $attributes['taxonomy'] ) + ? sanitize_key( $attributes['taxonomy'] ) + : 'category'; + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'number' => $per_page, + 'hide_empty' => false, + 'fields' => 'id=>name', + ) + ); + + if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { + return rest_ensure_response( array() ); + } + + $items = array(); + foreach ( $terms as $term_id => $term_name ) { + $items[] = array( + 'id' => (int) $term_id, + 'name' => (string) $term_name, + 'type' => 'term', + ); + } + return rest_ensure_response( $items ); + } + + return rest_ensure_response( array() ); + } + + /** + * Returns the current filter index status. + * + * @return \WP_REST_Response + */ + public function handle_filter_status() { + return rest_ensure_response( FilterIndexRebuilder::status() ); + } + + /** + * Runs a full filter index rebuild synchronously and returns the result. + * + * Note: on large sites this may approach PHP's max_execution_time. + * For v2.2 the synchronous model is acceptable; the dashboard polls + * /filter-status every 2 s so even a timeout is handled gracefully. + * + * @return \WP_REST_Response + */ + public function handle_filter_rebuild() { + return rest_ensure_response( FilterIndexRebuilder::rebuild_all() ); + } + + /** + * Returns all registered filters. + * + * @return \WP_REST_Response + */ + public function handle_filters_list() { + return rest_ensure_response( FilterRegistry::all() ); + } + + /** + * Unregisters a filter by key. + * + * @param \WP_REST_Request $request The REST request. + * @return \WP_REST_Response|\WP_Error + */ + public function handle_filter_unregister( \WP_REST_Request $request ) { + $key = $request->get_param( 'filter_key' ); + + if ( empty( $key ) ) { + return new \WP_Error( + 'airowp_filter_unregister_invalid', + __( 'filter_key is required.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + if ( null === FilterRegistry::get( $key ) ) { + return new \WP_Error( + 'airowp_filter_not_found', + __( 'Filter not found.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + FilterRegistry::unregister( $key ); + + return rest_ensure_response( + array( + 'unregistered' => true, + 'filter_key' => $key, + ) + ); + } + + /** + * Checks that the request carries a valid nonce and the user can edit posts. + * + * Used by the /query/preview route — any editor-level user may use the live + * preview endpoint; only admins may mutate the filter registry (see + * check_manage_options_permission). + * + * @param \WP_REST_Request $request The REST request. + * @return true|\WP_Error + */ + public function check_edit_posts_permission( \WP_REST_Request $request ) { + if ( ! is_user_logged_in() ) { + return new \WP_Error( + 'rest_forbidden', + __( 'You must be logged in.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + $nonce = $request->get_header( 'X-WP-Nonce' ); + if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { + return new \WP_Error( + 'rest_forbidden', + __( 'Invalid nonce.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + if ( ! current_user_can( 'edit_posts' ) ) { + return new \WP_Error( + 'rest_forbidden', + __( 'Insufficient permissions.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Handles the filter-register REST request. + * + * Stores the filter configuration in FilterRegistry so the PHP filter index + * knows how to resolve values for this filter key. + * + * @param \WP_REST_Request $request The REST request. + * @return \WP_REST_Response|\WP_Error + */ + public function handle_filter_register( \WP_REST_Request $request ) { + $key = $request->get_param( 'filter_key' ); + $config = (array) $request->get_param( 'config' ); + + if ( empty( $key ) || empty( $config['type'] ) || empty( $config['source'] ) ) { + return new \WP_Error( + 'airowp_filter_register_invalid', + __( 'filter_key, type, and source are required.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + $type = (string) $config['type']; + if ( ! in_array( $type, array( 'taxonomy', 'meta' ), true ) ) { + return new \WP_Error( + 'airowp_filter_invalid_type', + __( 'config.type must be "taxonomy" or "meta".', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + FilterRegistry::register( $key, $config ); + + return rest_ensure_response( + array( + 'registered' => true, + 'filter_key' => sanitize_key( $key ), + 'config' => FilterRegistry::get( $key ), + ) + ); + } + + /** + * Checks that the request is authenticated and carries a valid nonce. + * + * @param \WP_REST_Request $request The REST request. + * @return true|\WP_Error + */ + public function check_permission( \WP_REST_Request $request ) { + if ( ! is_user_logged_in() ) { + return new \WP_Error( + 'rest_forbidden', + __( 'You must be logged in.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + $nonce = $request->get_header( 'X-WP-Nonce' ); + if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { + return new \WP_Error( + 'rest_forbidden', + __( 'Invalid nonce.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + if ( ! current_user_can( 'read' ) ) { + return new \WP_Error( + 'rest_forbidden', + __( 'Insufficient permissions.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Handles the render REST request and returns HTML + pagination metadata. + * + * @param \WP_REST_Request $request The REST request. + * @return \WP_REST_Response + */ + public function handle_render( \WP_REST_Request $request ) { + $query_id = $request->get_param( 'queryId' ); + $attributes = (array) $request->get_param( 'attributes' ); + $page = max( 1, (int) $request->get_param( 'page' ) ); + $inner_html = (string) $request->get_param( 'innerBlocks' ); + $params = (array) $request->get_param( 'params' ); + $current_url = (string) $request->get_param( 'currentUrl' ); + + // Sibling filter blocks (search / sort / checkbox / select / active / + // reset) read filter state from $_GET and the current page URL + // (`add_query_arg(array())`) so the no-JS fallback can build chip/ + // reset links that navigate back to the page. On the REST refresh + // path, $_GET is empty and REQUEST_URI points at the REST endpoint, + // so we overlay both for the duration of the render and restore + // afterwards to avoid leaking state into later request-scoped code. + $original_get = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + // Snapshot REQUEST_URI raw — sanitize_text_field() mangles URL encoding + // (eats `+`, collapses whitespace) and this value is only ever restored + // to the superglobal, never echoed or used in HTML. + $original_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- restore-only, see comment above. + $allowed_keys = apply_filters( 'airowp_query_url_params', array( 'q', 'sort' ) ); + foreach ( $params as $key => $value ) { + $key = (string) $key; + if ( in_array( $key, $allowed_keys, true ) || 0 === strpos( $key, 'filter_' ) ) { + // REST-supplied values are sanitized downstream before use in + // WP_Query / SQL / HTML, but nested block renders may pass + // through filter hooks or third-party code that reads $_GET + // directly — sanitize at the overlay boundary too. + if ( is_array( $value ) ) { + $_GET[ $key ] = array_map( 'sanitize_text_field', wp_unslash( (array) $value ) ); + } else { + $_GET[ $key ] = sanitize_text_field( wp_unslash( (string) $value ) ); + } + } + } + if ( '' !== $current_url ) { + $parsed = wp_parse_url( $current_url ); + if ( is_array( $parsed ) && isset( $parsed['path'] ) ) { + $_SERVER['REQUEST_URI'] = $parsed['path'] + . ( isset( $parsed['query'] ) ? '?' . $parsed['query'] : '' ); + } + } + + try { + $result = self::render( + $attributes, + array( + 'query_id' => $query_id, + 'page' => $page, + 'inner_html' => $inner_html, + 'params' => $params, + ) + ); + } finally { + $_GET = $original_get; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $_SERVER['REQUEST_URI'] = $original_uri; + } + + return rest_ensure_response( $result ); + } + + /** + * Shared render entrypoint used by both REST and first-paint render.php. + * + * Delegates to airowp_query_render_region() so the REST response + * contains the full region (list + sibling blocks wrapped in + * .airo-wp-query-region) — identical to the first-paint output. The JS + * refresh handler swaps the outer region's innerHTML in one operation, + * updating pagination + no-results + chips together with the list. + * + * @param array $attributes Block attributes. + * @param array $context Keys: query_id, page, inner_html (full serialized + * innerBlocks including siblings), params. + * @return array { html: string, totalPages: int, totalItems: int } + */ + public static function render( array $attributes, array $context ) { + $helpers = AIRO_WP_PLUGIN_DIR . 'build/blocks/query/render-helpers.php'; + if ( file_exists( $helpers ) ) { + require_once $helpers; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- build artifact; path resolved from plugin directory + if ( function_exists( 'airowp_query_render_region' ) ) { + return airowp_query_render_region( $attributes, $context ); + } + // Fallback to bare render (no region wrapper) for environments where + // the build artefact predates the region helper (e.g. older build cache). + if ( function_exists( 'airowp_query_render' ) ) { + return airowp_query_render( $attributes, $context ); + } + } + return array( + 'html' => '', + 'totalPages' => 0, + 'totalItems' => 0, + ); + } +} diff --git a/includes/Blocks/Common/Query/FilterIndex.php b/includes/Blocks/Common/Query/FilterIndex.php new file mode 100644 index 0000000..d8c315b --- /dev/null +++ b/includes/Blocks/Common/Query/FilterIndex.php @@ -0,0 +1,517 @@ +prefix}airowp_query_filter_index custom table. + * + * @package airo-wp + * @since 2.2.0 + */ + +declare(strict_types=1); + +namespace GoDaddy\WordPress\Plugins\AiroWp\Blocks\Common\Query; + +defined( 'ABSPATH' ) || exit; +/** + * Manages the airowp_query_filter_index database table. + */ +class FilterIndex { + + /** + * Current schema version. + * + * Increment when columns or indexes change and add migration logic in + * install() before updating the stored option. + * + * v2 (2026-04-19): adds `post_type` column so count_for_options can scope + * per-CPT. Upgrade TRUNCATEs the table — see install(). + */ + const SCHEMA_VERSION = '2'; + + /** + * Option key that stores the installed schema version. + */ + const OPTION_SCHEMA = 'airowp_query_filter_index_schema'; + + /** + * Option key used to record background index status (e.g. "indexing", "ready"). + * Reserved for future reindex tasks (Task A2+). + */ + const OPTION_STATUS = 'airowp_query_filter_index_status'; + + /** + * Object-cache group for count_for_options() results. + * + * Cache entries carry the current epoch in their key so a bump invalidates + * every outstanding entry without needing to enumerate keys (which many + * object-cache backends do not support). + */ + const CACHE_GROUP = 'airowp_query_filter_index'; + + /** + * Cache-epoch key. Read via wp_cache_get / bumped via wp_cache_incr so a + * persistent object cache (Redis/Memcached) invalidates count results + * atomically without hitting the options table on each write. + */ + const CACHE_EPOCH_KEY = 'counts_epoch'; + + /** + * TTL (seconds) for cached count results. Counts stay correct until the + * next write (via the epoch), but we still TTL-cap in case the epoch + * counter is lost (non-persistent cache restart) — bounded staleness. + */ + const CACHE_TTL = 300; + + /** + * Per-request cache of the table existence check. + * + * Null = not yet checked; true/false = checked result. + * + * @var bool|null + */ + private static $table_exists = null; + + /** + * Returns the fully-qualified table name. + * + * @return string + */ + public static function table_name(): string { + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + return $wpdb->prefix . 'airowp_query_filter_index'; + } + + /** + * Returns true when the filter index table actually exists in the database. + * + * Result is cached for the lifetime of the request to avoid repeated + * SHOW TABLES queries. Call reset_table_cache() in tests between cases. + * + * @return bool + */ + public static function table_exists(): bool { + if ( null !== self::$table_exists ) { + return self::$table_exists; + } + global $wpdb; + $table_name = self::table_name(); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + self::$table_exists = ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ) === $table_name ); + return self::$table_exists; + } + + /** + * Resets the per-request table existence cache (for tests). + * + * @return void + */ + public static function reset_table_cache(): void { + self::$table_exists = null; + } + + /** + * Reindexes a single object's filter values. + * + * Deletes all prior rows for this (object_type, object_id) and rewrites them + * based on the current FilterRegistry entries. Idempotent by design. + * + * @param string $object_type One of 'post' (A2), 'user' (v2.4+), 'term' (v2.4+). + * @param int $object_id The object's primary key. + */ + public static function reindex_object( string $object_type, int $object_id ): void { + global $wpdb; + if ( $object_id <= 0 || ! self::table_exists() ) { + return; + } + + // v2.2 indexes only published posts. If the post is not published, + // remove any existing rows and bail. This covers the case where + // taxonomy/meta hooks fire for a draft that was previously published. + $post_type = ''; + if ( 'post' === $object_type ) { + $post = get_post( $object_id ); + if ( ! $post || 'publish' !== $post->post_status ) { + self::remove_object( 'post', $object_id ); + return; + } + $post_type = (string) $post->post_type; + } + + $table = self::table_name(); + + // Idempotency: wipe existing rows for this object before reinsert. + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->delete( + $table, + array( + 'object_id' => $object_id, + 'object_type' => $object_type, + ), + array( '%d', '%s' ) + ); + + $filters = FilterRegistry::all(); + if ( empty( $filters ) ) { + return; + } + + $rows = array(); + foreach ( $filters as $filter_key => $config ) { + $values = self::resolve_filter_values( $object_type, $object_id, $config ); + foreach ( $values as $value ) { + $rows[] = array( + 'object_id' => $object_id, + 'object_type' => $object_type, + 'post_type' => $post_type, + 'filter_key' => $filter_key, + 'filter_value' => (string) $value, + ); + } + } + + if ( empty( $rows ) ) { + return; + } + + // Bulk insert — one query regardless of filter count. + $placeholders = array(); + $params = array(); + foreach ( $rows as $row ) { + $placeholders[] = '(%d, %s, %s, %s, %s)'; + $params[] = $row['object_id']; + $params[] = $row['object_type']; + $params[] = $row['post_type']; + $params[] = $row['filter_key']; + $params[] = $row['filter_value']; + } + + $sql = "INSERT INTO {$table} (object_id, object_type, post_type, filter_key, filter_value) VALUES " + . implode( ', ', $placeholders ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->query( $wpdb->prepare( $sql, $params ) ); + + self::bump_counts_cache(); + } + + /** + * Resolves filter values for a given object. Posts-only in v2.2. + * + * @param string $object_type One of 'post', 'user', 'term'. + * @param int $object_id Object primary key. + * @param array $config Filter config from FilterRegistry: { type, source, label }. + * @return array Flat array of string values to index. + */ + private static function resolve_filter_values( string $object_type, int $object_id, array $config ): array { + if ( 'post' !== $object_type ) { + return array(); // v2.4 will add user/term support. + } + + $type = $config['type'] ?? ''; + $source = $config['source'] ?? ''; + if ( '' === $source ) { + return array(); + } + + if ( 'taxonomy' === $type ) { + $term_ids = wp_get_post_terms( $object_id, $source, array( 'fields' => 'ids' ) ); + if ( is_wp_error( $term_ids ) || empty( $term_ids ) ) { + return array(); + } + return array_map( 'strval', $term_ids ); + } + + if ( 'meta' === $type ) { + $meta = get_post_meta( $object_id, $source, false ); + if ( ! is_array( $meta ) || empty( $meta ) ) { + return array(); + } + // Filter out empty strings, non-scalars, and values exceeding the VARCHAR(190) column width. + $clean = array(); + foreach ( $meta as $value ) { + if ( ! is_scalar( $value ) ) { + continue; + } + $value = (string) $value; + if ( '' === $value ) { + continue; + } + if ( mb_strlen( $value ) > 190 ) { + continue; // Longer than the VARCHAR(190) index column; skip to avoid truncation. + } + $clean[] = $value; + } + return $clean; + } + + return array(); + } + + /** + * Deletes all index rows for a given object. + * + * Scoped by both object_id and object_type to avoid cross-type collisions + * (e.g. a post and a user that happen to share the same numeric ID). + * + * @param string $object_type The object type (e.g. 'post'). + * @param int $object_id The object's primary key. + * @return void + */ + public static function remove_object( string $object_type, int $object_id ): void { + if ( ! self::table_exists() ) { + return; + } + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->delete( + self::table_name(), + array( + 'object_id' => $object_id, + 'object_type' => $object_type, + ), + array( '%d', '%s' ) + ); + self::bump_counts_cache(); + } + + /** + * Returns the count of distinct objects matching each option value for a + * filter key, intersected with the current active-filter state. + * + * Within-group semantics: selections inside the same filter group are OR + * (showing "how many objects would match if you added this value"). + * Across-group semantics: each other active-filter group is AND. + * The self-filter is excluded from the intersection so users can still see + * counts for all options of the group they are currently filtering on. + * + * @param string $filter_key The filter key to count options for (e.g. 'category'). + * @param array $option_values Option values to count. Values are (string)-cast. + * @param array $active_filters Active filter state: [ filter_key => [ value, ... ] ]. + * @param string $post_type Optional. When non-empty, counts are restricted + * to rows with a matching post_type — and so are + * the intersection subqueries for active filters, + * so a CPT-scoped query never leaks counts from + * other post types that share the same taxonomy. + * @return array [ value => count ] zero-filled for options absent from the result set. + */ + public static function count_for_options( string $filter_key, array $option_values, array $active_filters, string $post_type = '' ): array { + if ( empty( $option_values ) || ! self::table_exists() ) { + return array(); + } + + global $wpdb; + $table = self::table_name(); + $key = sanitize_key( $filter_key ); + if ( '' === $key ) { + return array(); + } + + // Normalise option values to strings and build a keyed default (0-filled). + $string_values = array_values( array_unique( array_map( 'strval', $option_values ) ) ); + $counts = array_fill_keys( $string_values, 0 ); + + // Exclude self-filter from intersection — OR semantics within a group. + unset( $active_filters[ $key ] ); + + // Cache lookup. Key is (epoch, filter_key, option_values, active_filters, post_type). + // The epoch is bumped on every index write, so cached entries remain valid + // until the next reindex/remove/rebuild. Skipped in tests where the + // per-request table_exists cache can race with the drop-table teardown. + $cache_key = self::build_counts_cache_key( $key, $string_values, $active_filters, $post_type ); + $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); + if ( is_array( $cached ) ) { + return $cached; + } + + // Optional post_type scope — applied to the outer query AND each + // intersection subquery so cross-filter counts stay within the CPT. + $post_type_sql = ''; + $subquery_pt_sql = ''; + $post_type_params = array(); + if ( '' !== $post_type ) { + $post_type_sql = ' AND post_type = %s'; + $subquery_pt_sql = ' AND post_type = %s'; + $post_type_params[] = $post_type; + } + + // Build intersection subqueries, one per active-filter group. + $intersect_sql = ''; + $intersect_params = array(); + + foreach ( $active_filters as $f_key => $f_values ) { + $sanitized_f_key = sanitize_key( (string) $f_key ); + if ( '' === $sanitized_f_key || empty( $f_values ) ) { + continue; + } + $f_strings = array_values( array_unique( array_map( 'strval', (array) $f_values ) ) ); + if ( empty( $f_strings ) ) { + continue; + } + $f_placeholders = implode( ',', array_fill( 0, count( $f_strings ), '%s' ) ); + $intersect_sql .= " AND object_id IN ( + SELECT object_id FROM {$table} + WHERE filter_key = %s AND filter_value IN ({$f_placeholders}){$subquery_pt_sql} + )"; + $intersect_params[] = $sanitized_f_key; + foreach ( $f_strings as $v ) { + $intersect_params[] = $v; + } + if ( '' !== $post_type ) { + $intersect_params[] = $post_type; + } + } + + $value_placeholders = implode( ',', array_fill( 0, count( $string_values ), '%s' ) ); + $sql = "SELECT filter_value, COUNT(DISTINCT object_id) AS cnt + FROM {$table} + WHERE filter_key = %s AND filter_value IN ({$value_placeholders}){$post_type_sql} + {$intersect_sql} + GROUP BY filter_value"; + + $params = array_merge( array( $key ), $string_values, $post_type_params, $intersect_params ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ) ); + + if ( is_array( $rows ) ) { + foreach ( $rows as $row ) { + if ( array_key_exists( $row->filter_value, $counts ) ) { + $counts[ $row->filter_value ] = (int) $row->cnt; + } + } + } + + wp_cache_set( $cache_key, $counts, self::CACHE_GROUP, self::CACHE_TTL ); // phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined -- CACHE_TTL = 300 seconds (VIP minimum) + + return $counts; + } + + /** + * Builds a cache key for count_for_options() that scopes the cached value to + * the current cache epoch + the full argument shape. Any index write bumps + * the epoch, so stale entries become unreachable without explicit deletion. + * + * @param string $filter_key Sanitized filter key. + * @param array $string_values Sorted, unique option values. + * @param array $active_filters Active filter state (minus self-filter). + * @param string $post_type Post-type scope or empty string for all. + * @return string + */ + private static function build_counts_cache_key( string $filter_key, array $string_values, array $active_filters, string $post_type = '' ): string { + $epoch = self::get_counts_epoch(); + // Sort values and active filters for stable key regardless of input order. + sort( $string_values ); + ksort( $active_filters ); + foreach ( $active_filters as $k => $vs ) { + if ( is_array( $vs ) ) { + $vs = array_values( array_unique( array_map( 'strval', $vs ) ) ); + sort( $vs ); + $active_filters[ $k ] = $vs; + } + } + return 'cfo:' . $epoch . ':' . md5( + $filter_key . '|' . wp_json_encode( $string_values ) . '|' . wp_json_encode( $active_filters ) . '|pt=' . $post_type + ); + } + + /** + * Returns the current counts cache epoch — an incrementing integer used as + * part of the cache key so bumping the epoch invalidates every outstanding + * cached result without key enumeration. + * + * @return int + */ + private static function get_counts_epoch(): int { + $epoch = wp_cache_get( self::CACHE_EPOCH_KEY, self::CACHE_GROUP ); + if ( false === $epoch ) { + $epoch = 1; + wp_cache_set( self::CACHE_EPOCH_KEY, $epoch, self::CACHE_GROUP ); + } + return (int) $epoch; + } + + /** + * Bumps the counts cache epoch, invalidating all previously-cached + * count_for_options() results. Called from reindex_object / remove_object + * and by the rebuilder on completion. Object-cache-only (no DB write) so + * bumping inside tight loops is safe; with a persistent cache backend the + * bump is atomic via wp_cache_incr. + * + * @return void + */ + public static function bump_counts_cache(): void { + $incremented = wp_cache_incr( self::CACHE_EPOCH_KEY, 1, self::CACHE_GROUP ); + if ( false === $incremented ) { + // Non-persistent caches (or no cache yet) — seed then bump. + $current = (int) wp_cache_get( self::CACHE_EPOCH_KEY, self::CACHE_GROUP ); + wp_cache_set( self::CACHE_EPOCH_KEY, $current + 1, self::CACHE_GROUP ); + } + } + + /** + * Returns true if the given filter key is registered in FilterRegistry. + * + * @param string $filter_key The filter key to check (e.g. 'category'). + * @return bool + */ + public static function is_available( string $filter_key ): bool { + return null !== FilterRegistry::get( $filter_key ); + } + + /** + * Creates or upgrades the filter index table via dbDelta. + * + * Safe to call multiple times — dbDelta is idempotent. + * + * @return void + */ + public static function install(): void { + global $wpdb; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + $table = self::table_name(); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $charset = $wpdb->get_charset_collate(); + $stored_schema = (string) get_option( self::OPTION_SCHEMA, '0' ); + $needs_truncate = self::table_exists() && version_compare( $stored_schema, '2', '<' ); + + // Note: PRIMARY KEY requires two spaces before the column name — dbDelta quirk. + // post_type is VARCHAR(40) to match WP's wp_posts.post_type column width. + // Empty string for non-post object_types (users/terms in v2.4+). + $sql = "CREATE TABLE {$table} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + object_id BIGINT UNSIGNED NOT NULL, + object_type VARCHAR(20) NOT NULL, + post_type VARCHAR(40) NOT NULL DEFAULT '', + filter_key VARCHAR(190) NOT NULL, + filter_value VARCHAR(190) NOT NULL, + indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY filter_key_value (filter_key, filter_value), + KEY filter_scope (post_type, filter_key, filter_value), + KEY object_lookup (object_type, object_id) +) {$charset};"; + + dbDelta( $sql ); + + // On a v1 → v2 upgrade, pre-existing rows have post_type='' (dbDelta's + // DEFAULT fills new columns). A per-CPT count query would miss them, + // so TRUNCATE and rely on the admin dashboard / WP-CLI rebuild to + // reindex with correct post_type values. v2.2 has not shipped yet so + // no production data is lost. + if ( $needs_truncate ) { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', $table ) ); + self::bump_counts_cache(); + } + + // Reset the per-request cache so subsequent calls see the new table. + self::$table_exists = null; + + update_option( self::OPTION_SCHEMA, self::SCHEMA_VERSION, false ); + } +} diff --git a/includes/Blocks/Common/Query/FilterIndexCLI.php b/includes/Blocks/Common/Query/FilterIndexCLI.php new file mode 100644 index 0000000..ed3e60b --- /dev/null +++ b/includes/Blocks/Common/Query/FilterIndexCLI.php @@ -0,0 +1,168 @@ +] + * : Posts to process per batch. Default 200, min 50. + * + * ## EXAMPLES + * + * wp dsgo query index rebuild + * wp dsgo query index rebuild --batch-size=500 + * + * @param array $args Positional arguments (unused). + * @param array $assoc_args Named arguments (batch-size). + */ + public function rebuild( $args, $assoc_args ): void { + $batch = (int) \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 200 ); + $result = FilterIndexRebuilder::rebuild_all( array( 'batch_size' => $batch ) ); + + if ( 'error' === ( $result['status'] ?? '' ) ) { + \WP_CLI::error( sprintf( 'Rebuild failed (status: %s).', $result['status'] ) ); + } + + \WP_CLI::success( + sprintf( + 'Indexed %d objects (%d rows).', + (int) $result['processed'], + (int) $result['total_rows'] + ) + ); + } + + /** + * Rebuild a single filter. + * + * ## OPTIONS + * + * + * : The filter key to rebuild (e.g. 'category', 'post_tag', 'price'). + * + * [--batch-size=] + * : Posts to process per batch. Default 200. + * + * ## EXAMPLES + * + * wp dsgo query index rebuild-filter category + * wp dsgo query index rebuild-filter price --batch-size=500 + * + * @subcommand rebuild-filter + * + * @param array $args Positional arguments: $args[0] = filter_key. + * @param array $assoc_args Named arguments (batch-size). + */ + public function rebuild_filter( $args, $assoc_args ): void { + if ( empty( $args[0] ) ) { + \WP_CLI::error( 'Filter key is required.' ); + } + + $batch = (int) \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 200 ); + $result = FilterIndexRebuilder::rebuild_filter( $args[0], array( 'batch_size' => $batch ) ); + + if ( 'skipped' === ( $result['status'] ?? '' ) ) { + \WP_CLI::warning( sprintf( 'Filter "%s" is not registered — nothing to do.', $args[0] ) ); + return; + } + + if ( 'error' === ( $result['status'] ?? '' ) ) { + \WP_CLI::error( 'Rebuild failed.' ); + } + + \WP_CLI::success( + sprintf( + 'Rebuilt filter "%s" (%d objects, %d rows).', + $args[0], + (int) $result['processed'], + (int) $result['total_rows'] + ) + ); + } + + /** + * Show current filter index status. + * + * ## EXAMPLES + * + * wp dsgo query index status + */ + public function status(): void { + $status = FilterIndexRebuilder::status(); + + if ( ! empty( $status['last_rebuilt_at'] ) && is_numeric( $status['last_rebuilt_at'] ) ) { + $status['last_rebuilt_at'] = gmdate( 'Y-m-d H:i:s', (int) $status['last_rebuilt_at'] ) . ' UTC'; + } elseif ( empty( $status['last_rebuilt_at'] ) ) { + $status['last_rebuilt_at'] = 'never'; + } + + $status['in_progress'] = $status['in_progress'] ? 'yes' : 'no'; + + \WP_CLI\Utils\format_items( 'table', array( $status ), array_keys( $status ) ); + } + + /** + * Drop the filter index table and clear its options. + * + * ## OPTIONS + * + * [--yes] + * : Skip the confirmation prompt. + * + * ## EXAMPLES + * + * wp dsgo query index drop + * wp dsgo query index drop --yes + * + * @param array $args Positional arguments (unused). + * @param array $assoc_args Named arguments (yes). + */ + public function drop( $args, $assoc_args ): void { + \WP_CLI::confirm( 'This will drop the filter index table and all its data. Continue?', $assoc_args ); + + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', FilterIndex::table_name() ) ); + delete_option( FilterIndex::OPTION_SCHEMA ); + delete_option( FilterIndex::OPTION_STATUS ); + // Also clear the plugin db version so the next admin_init fires + // maybe_upgrade() and reinstalls the table. Without this, the stored + // version is still '2.2.0' and the install logic is skipped. + delete_option( 'airowp_db_version' ); + + \WP_CLI::success( 'Filter index table dropped.' ); + } +} diff --git a/includes/Blocks/Common/Query/FilterIndexHooks.php b/includes/Blocks/Common/Query/FilterIndexHooks.php new file mode 100644 index 0000000..0d918a6 --- /dev/null +++ b/includes/Blocks/Common/Query/FilterIndexHooks.php @@ -0,0 +1,217 @@ +post_status ) { + FilterIndex::remove_object( 'post', $post_id ); + return; + } + FilterIndex::reindex_object( 'post', $post_id ); + self::register_filters_from_post_blocks( $post ); + } + + /** + * Parses a published post's block content and registers any + * airo-wp/query-filter blocks' filters into the FilterRegistry. + * + * Moving registration to save time (rather than the editor) ensures that + * only filters belonging to actually-published posts end up in the registry. + * Abandoned drafts no longer pollute the registry. + * + * @param \WP_Post $post The published post whose content to scan. + * @return void + */ + private static function register_filters_from_post_blocks( \WP_Post $post ): void { + if ( ! function_exists( 'parse_blocks' ) ) { + return; + } + $blocks = parse_blocks( $post->post_content ); + self::walk_blocks_for_filters( $blocks ); + } + + /** + * Recursively walks a block tree and registers taxonomy filters for any + * airo-wp/query-filter blocks found. + * + * @param array $blocks Parsed block array from parse_blocks(). + * @return void + */ + private static function walk_blocks_for_filters( array $blocks ): void { + foreach ( $blocks as $block ) { + if ( 'airo-wp/query-filter' === ( $block['blockName'] ?? '' ) ) { + $attrs = $block['attrs'] ?? array(); + $taxonomy = sanitize_key( (string) ( $attrs['taxonomy'] ?? '' ) ); + if ( '' !== $taxonomy ) { + FilterRegistry::register( + $taxonomy, + array( + 'type' => 'taxonomy', + 'source' => $taxonomy, + ) + ); + } + } + if ( ! empty( $block['innerBlocks'] ) ) { + self::walk_blocks_for_filters( $block['innerBlocks'] ); + } + } + } + + /** + * Removes a post's index rows when the post is force-deleted. + * + * @param int $post_id The post ID that was deleted. + * @return void + */ + public static function on_deleted_post( int $post_id ): void { + FilterIndex::remove_object( 'post', $post_id ); + } + + /** + * Reindexes a post when its taxonomy terms are changed, but only when + * the taxonomy is tracked by at least one registered filter. + * + * Set_object_terms fires for any taxonomy-capable object; we only index + * posts in v2.2, so we short-circuit when the object ID does not resolve + * to a real post. + * + * @param int $object_id The object ID whose terms changed. + * @param array $terms Unused — new term IDs (passed by WP hook). + * @param array $tt_ids Unused — new term-taxonomy IDs (passed by WP hook). + * @param string $taxonomy The taxonomy slug that was updated. + * @return void + */ + public static function on_set_object_terms( int $object_id, array $terms, array $tt_ids, string $taxonomy ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + // set_object_terms fires for any taxonomy-capable object; we only index posts in v2.2. + if ( ! get_post( $object_id ) ) { + return; + } + foreach ( FilterRegistry::all() as $config ) { + if ( 'taxonomy' === ( $config['type'] ?? '' ) && ( $config['source'] ?? '' ) === $taxonomy ) { + FilterIndex::reindex_object( 'post', $object_id ); + return; + } + } + } + + /** + * Reindexes a post when one of its meta values changes, but only when + * the meta key is tracked by at least one registered filter. + * + * @param int|string $meta_id Meta row ID (unused; required by hook signature). + * @param int $object_id Post ID whose meta changed. + * @param string $meta_key The changed meta key. + * @return void + */ + public static function on_post_meta_changed( $meta_id, int $object_id, string $meta_key ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + foreach ( FilterRegistry::all() as $config ) { + if ( 'meta' === ( $config['type'] ?? '' ) && ( $config['source'] ?? '' ) === $meta_key ) { + FilterIndex::reindex_object( 'post', $object_id ); + return; + } + } + } +} diff --git a/includes/Blocks/Common/Query/FilterIndexRebuilder.php b/includes/Blocks/Common/Query/FilterIndexRebuilder.php new file mode 100644 index 0000000..45642d3 --- /dev/null +++ b/includes/Blocks/Common/Query/FilterIndexRebuilder.php @@ -0,0 +1,433 @@ + 0 && ( time() - $locked_at ) < self::LOCK_TTL; + } + + /** + * Attempts to acquire the rebuild mutex atomically. + * + * `add_option` is backed by an `INSERT ... ON DUPLICATE KEY UPDATE` no-op at + * the MySQL level, giving us atomic gate semantics with no TOCTOU window. + * If the option already exists we check whether the stored timestamp is + * stale (older than LOCK_TTL) and retry once if so, so a crashed prior + * process can't deadlock future rebuilds. + * + * @return bool True when the lock was acquired; false when another active + * process genuinely holds it. + */ + private static function acquire_lock(): bool { + if ( add_option( self::LOCK_OPTION, time(), '', false ) ) { + return true; + } + // add_option failed — option exists. Honour an active lock. + if ( self::is_locked() ) { + return false; + } + // Stale lock — clear and retry once. A concurrent retry will win the race. + delete_option( self::LOCK_OPTION ); + return (bool) add_option( self::LOCK_OPTION, time(), '', false ); + } + + /** + * Releases the rebuild mutex by deleting the lock option. + * + * @return void + */ + private static function release_lock(): void { + delete_option( self::LOCK_OPTION ); + } + + /** + * Truncates the index and repopulates it from all published posts. + * + * Batch-scans the posts table in chunks of $args['batch_size'] (default 200, + * minimum 50). Writes progress to FilterIndex::OPTION_STATUS so callers can + * poll status() during a long run (A7 / B5). + * + * @param array $args { + * Optional overrides. + * @type int $batch_size Number of posts per iteration. Default 200, min 50. + * } + * @return array { + * @type string $status 'complete' or 'locked'. + * @type int $processed Number of post IDs iterated. + * @type int $total_rows Total index rows after rebuild. + * } + */ + public static function rebuild_all( array $args = array() ): array { + if ( ! self::acquire_lock() ) { + return array( + 'status' => 'locked', + 'processed' => 0, + 'total_rows' => 0, + ); + } + + try { + return self::do_rebuild_all( $args ); + } finally { + self::release_lock(); + } + } + + /** + * Inner implementation of rebuild_all — called inside the mutex. + * + * @param array $args Optional overrides (batch_size). + * @return array Result array (status, processed, total_rows). + */ + private static function do_rebuild_all( array $args ): array { + global $wpdb; + $batch_size = max( self::MIN_BATCH_SIZE, (int) ( $args['batch_size'] ?? self::DEFAULT_BATCH_SIZE ) ); + $started_at = microtime( true ); + + self::write_status( + array( + 'in_progress' => true, + 'started_at' => time(), + 'processed' => 0, + ) + ); + + // Short-circuit with a clean error status if the table hasn't been + // installed yet. Without this guard the TRUNCATE below emits a loud + // "table doesn't exist" notice before write_status runs. + FilterIndex::reset_table_cache(); + if ( ! FilterIndex::table_exists() ) { + self::write_status( + array( + 'in_progress' => false, + 'error' => 'table_missing', + 'updated_at' => time(), + ) + ); + return array( + 'status' => 'error', + 'processed' => 0, + 'total_rows' => 0, + ); + } + + $table = FilterIndex::table_name(); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $truncated = $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', $table ) ); + FilterIndex::bump_counts_cache(); + if ( false === $truncated ) { + self::write_status( + array( + 'in_progress' => false, + 'error' => 'truncate_failed', + 'updated_at' => time(), + ) + ); + return array( + 'status' => 'error', + 'processed' => 0, + 'total_rows' => 0, + ); + } + + // Keyset pagination: track the last ID seen and query `WHERE ID > $last_id`. + // OFFSET degrades quadratically because MySQL must scan all skipped rows; + // keyset uses the `posts.ID` PK index to jump directly to the next batch. + $processed = 0; + $last_id = 0; + $ids_count = 0; + do { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $ids = $wpdb->get_col( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish' AND ID > %d ORDER BY ID ASC LIMIT %d", + $last_id, + $batch_size + ) + ); + + foreach ( $ids as $id ) { + $id = (int) $id; + FilterIndex::reindex_object( 'post', $id ); + if ( $id > $last_id ) { + $last_id = $id; + } + ++$processed; + } + + $ids_count = count( $ids ); + self::write_status( + array( + 'in_progress' => $ids_count === $batch_size, + 'processed' => $processed, + 'updated_at' => time(), + ) + ); + } while ( $ids_count === $batch_size ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $total_rows = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) ); + $duration_ms = (int) ( ( microtime( true ) - $started_at ) * 1000 ); + + self::write_status( + array( + 'in_progress' => false, + 'last_rebuilt_at' => time(), + 'duration_ms' => $duration_ms, + 'processed' => $processed, + 'total_rows' => $total_rows, + ) + ); + + return array( + 'status' => 'complete', + 'processed' => $processed, + 'total_rows' => $total_rows, + ); + } + + /** + * Wipes all index rows for a single filter key and repopulates them. + * + * Because reindex_object() rewrites ALL filters for a given post, calling + * this method will also refresh other filters' rows for each post it touches. + * Per-post partial reindex is deferred to v2.5+. + * + * Returns early with status='skipped' if the key is not registered. + * + * Processes posts in batches to avoid max_execution_time on large sites. + * Writes intermediate progress to FilterIndex::OPTION_STATUS so callers can + * poll status() during a long run (A7 / B5). + * + * @param string $filter_key The registered filter key to rebuild (e.g. 'category'). + * @param array $args { + * Optional overrides. + * @type int $batch_size Number of posts per iteration. Default 200, min 50. + * } + * @return array { + * @type string $status 'complete' or 'skipped'. + * @type int $processed Number of post IDs iterated (0 when skipped). + * @type int $total_rows Rows for this key after rebuild (0 when skipped). + * } + */ + public static function rebuild_filter( string $filter_key, array $args = array() ): array { + $key = sanitize_key( $filter_key ); + if ( '' === $key || null === FilterRegistry::get( $key ) ) { + return array( + 'status' => 'skipped', + 'processed' => 0, + 'total_rows' => 0, + ); + } + + if ( ! self::acquire_lock() ) { + return array( + 'status' => 'locked', + 'processed' => 0, + 'total_rows' => 0, + ); + } + + try { + return self::do_rebuild_filter( $key, $args ); + } finally { + self::release_lock(); + } + } + + /** + * Inner implementation of rebuild_filter — called inside the mutex. + * + * @param string $key Sanitized filter key. + * @param array $args Optional overrides (batch_size). + * @return array Result array (status, processed, total_rows). + */ + private static function do_rebuild_filter( string $key, array $args ): array { + global $wpdb; + $table = FilterIndex::table_name(); + $batch_size = max( self::MIN_BATCH_SIZE, (int) ( $args['batch_size'] ?? self::DEFAULT_BATCH_SIZE ) ); + + // Delete all rows for this key in one statement — fast regardless of row count. + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->delete( $table, array( 'filter_key' => $key ), array( '%s' ) ); + FilterIndex::bump_counts_cache(); + + self::write_status( + array( + 'in_progress' => true, + 'started_at' => time(), + 'processed' => 0, + ) + ); + + // Keyset pagination (see do_rebuild_all for rationale). + $processed = 0; + $last_id = 0; + $ids_count = 0; + do { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $ids = $wpdb->get_col( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $wpdb->prepare( + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish' AND ID > %d ORDER BY ID ASC LIMIT %d", + $last_id, + $batch_size + ) + ); + + foreach ( $ids as $id ) { + $id = (int) $id; + FilterIndex::reindex_object( 'post', $id ); + if ( $id > $last_id ) { + $last_id = $id; + } + ++$processed; + } + + $ids_count = count( $ids ); + self::write_status( + array( + 'in_progress' => $ids_count === $batch_size, + 'processed' => $processed, + 'updated_at' => time(), + ) + ); + } while ( $ids_count === $batch_size ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + $total_rows = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE filter_key = %s", $key ) ); + + self::write_status( + array( + 'in_progress' => false, + 'last_rebuilt_at' => time(), + 'processed' => $processed, + 'total_rows' => $total_rows, + ) + ); + + return array( + 'status' => 'complete', + 'processed' => $processed, + 'total_rows' => $total_rows, + ); + } + + /** + * Returns the current index status. + * + * Reads FilterIndex::OPTION_STATUS and supplements it with a live row count + * so the caller always gets an up-to-date snapshot without needing a full + * rebuild to have run first. + * + * @return array { + * @type int $total_rows Current row count in the index table. + * @type bool $in_progress Whether a rebuild is currently running. + * @type int|null $last_rebuilt_at Unix timestamp of the last completed rebuild, or null. + * @type int $processed Posts processed in the last (or current) rebuild. + * } + */ + public static function status(): array { + global $wpdb; + $status = get_option( FilterIndex::OPTION_STATUS, array() ); + if ( ! is_array( $status ) ) { + $status = array(); + } + + // Auto-clear stale in-progress state (timeout safeguard). Releasing the + // mutex at the same moment prevents the dashboard from showing "idle" + // while a later Rebuild click comes back with "locked" until LOCK_TTL + // also elapses — the two TTLs are aligned but a poll can still race + // a new click, so unlock explicitly. + if ( ! empty( $status['in_progress'] ) && ! empty( $status['started_at'] ) ) { + if ( time() - (int) $status['started_at'] > 5 * MINUTE_IN_SECONDS ) { + $status['in_progress'] = false; + $status['error'] = 'timed_out'; + update_option( FilterIndex::OPTION_STATUS, $status, false ); + self::release_lock(); + } + } + + // Always surface a live row count and normalise required keys. + $index_table = FilterIndex::table_name(); + $status['total_rows'] = FilterIndex::table_exists() + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + ? (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $index_table ) ) + : 0; + $status['in_progress'] = (bool) ( $status['in_progress'] ?? false ); + $status['last_rebuilt_at'] = $status['last_rebuilt_at'] ?? null; + $status['processed'] = (int) ( $status['processed'] ?? 0 ); + + return $status; + } + + /** + * Merges a patch array into the stored OPTION_STATUS option. + * + * Uses autoload=false because this option changes frequently during a + * rebuild and does not need to be loaded on every request. + * + * @param array $patch Key-value pairs to merge into the current status. + * @return void + */ + private static function write_status( array $patch ): void { + $current = get_option( FilterIndex::OPTION_STATUS, array() ); + if ( ! is_array( $current ) ) { + $current = array(); + } + update_option( FilterIndex::OPTION_STATUS, array_merge( $current, $patch ), false ); + } +} diff --git a/includes/Blocks/Common/Query/FilterRegistry.php b/includes/Blocks/Common/Query/FilterRegistry.php new file mode 100644 index 0000000..d4a17c4 --- /dev/null +++ b/includes/Blocks/Common/Query/FilterRegistry.php @@ -0,0 +1,181 @@ + sanitize_key( $config['type'] ?? '' ), + 'source' => sanitize_text_field( $config['source'] ?? '' ), + 'label' => sanitize_text_field( $config['label'] ?? $key ), + ); + + update_option( self::OPTION, $filters, false ); + self::$cache = null; + + // First-time registration: queue a background backfill of index rows + // for all existing posts. Without this, posts that predate the filter + // block (common when authors add the block to an already-populated + // site) show "(0)" next to terms they legitimately belong to because + // the save_post hooks only cover posts updated AFTER the filter was + // registered. + // + // Queued (not inline) because `rebuild_filter` iterates the entire + // posts table — on a large site that would stall the post-save + // request that triggered registration and could time out. WP-Cron + // runs the backfill on the next request instead. The hook is handled + // by FilterIndexHooks::on_filter_registered below. + if ( $is_new ) { + /** + * Fires when a new filter key is added to the registry. + * + * @param string $filter_key The sanitized filter key that was just registered. + */ + do_action( 'airowp_query_filter_registered', $sanitized_key ); + } + } + + /** + * Removes a filter from the option-backed registry. + * + * @param string $key The filter key to remove. + * @return void + */ + public static function unregister( string $key ): void { + $filters = get_option( self::OPTION, array() ); + if ( ! is_array( $filters ) ) { + return; + } + + $sanitized_key = sanitize_key( $key ); + if ( ! array_key_exists( $sanitized_key, $filters ) ) { + return; + } + unset( $filters[ $sanitized_key ] ); + + update_option( self::OPTION, $filters, false ); + self::$cache = null; + } + + /** + * Returns all registered filters, merged with any registered via filter. + * + * @return array Keyed array of filter configs (key => { type, source, label }). + */ + public static function all(): array { + self::ensure_hooks(); + + if ( null !== self::$cache ) { + return self::$cache; + } + + $stored = get_option( self::OPTION, array() ); + if ( ! is_array( $stored ) ) { + $stored = array(); + } + + /** + * Filter the registered filters. + * + * @since 2.2.0 + * + * @param array $stored Keyed array of filter configs (key => { type, source, label }). + */ + self::$cache = (array) apply_filters( 'airowp_query_registered_filters', $stored ); + + return self::$cache; + } + + /** + * Clears the per-request cache. Intended for test teardown and for callers + * that mutate the underlying option directly. + * + * @return void + */ + public static function bust_cache(): void { + self::$cache = null; + } + + /** + * Returns the config for a single registered filter, or null if not found. + * + * @param string $key Filter registry key. + * @return array|null Filter config array or null if the key is not registered. + */ + public static function get( string $key ): ?array { + $all = self::all(); + $sanitized_key = sanitize_key( $key ); + return $all[ $sanitized_key ] ?? null; + } +} diff --git a/includes/Blocks/Common/Query/TemplateController.php b/includes/Blocks/Common/Query/TemplateController.php new file mode 100644 index 0000000..3938b55 --- /dev/null +++ b/includes/Blocks/Common/Query/TemplateController.php @@ -0,0 +1,434 @@ + \WP_REST_Server::READABLE, + 'callback' => array( __CLASS__, 'export' ), + 'permission_callback' => array( __CLASS__, 'permission_export' ), + 'args' => array( + 'post_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + 'query_id' => array( + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( __CLASS__, 'import' ), + 'permission_callback' => array( __CLASS__, 'permission_import' ), + 'args' => array( + 'schemaVersion' => array( + 'required' => true, + 'type' => 'integer', + ), + 'blockName' => array( + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'attributes' => array( + 'required' => true, + 'type' => 'object', + ), + 'innerBlocks' => array( + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => array( __CLASS__, 'sanitize_inner_blocks_markup' ), + ), + ), + ), + ) + ); + } + + /** + * Normalizes imported inner block markup without KSES-stripping valid block HTML. + * + * This route returns markup to the editor; it does not persist post content. + * Re-serializing parsed blocks preserves custom/raw block HTML while still + * normalizing the block tree to canonical comment markup. + * + * @param mixed $value Raw `innerBlocks` request value. + * @return string + */ + public static function sanitize_inner_blocks_markup( $value ) { + if ( ! is_string( $value ) ) { + return ''; + } + + return self::serialize_blocks_markup( parse_blocks( $value ) ); + } + + /** + * Checks that the requesting user can edit the specified post. + * + * @param \WP_REST_Request $request Incoming REST request. + * @return true|\WP_Error + */ + public static function permission_export( \WP_REST_Request $request ) { + $nonce_check = self::verify_nonce( $request ); + if ( is_wp_error( $nonce_check ) ) { + return $nonce_check; + } + + $post_id = (int) $request['post_id']; + + if ( ! $post_id || ! get_post( $post_id ) ) { + return new \WP_Error( + 'airowp_template_no_post', + __( 'Post not found.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return new \WP_Error( + 'airowp_template_forbidden', + __( 'You do not have permission to export this post.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Checks that the requesting user can create/edit posts. + * + * Import is a generator (produces new markup), not a post mutator, so + * `edit_posts` is sufficient — no specific post_id to gate against. + * + * @param \WP_REST_Request $request Incoming REST request. + * @return true|\WP_Error + */ + public static function permission_import( \WP_REST_Request $request ) { + $nonce_check = self::verify_nonce( $request ); + if ( is_wp_error( $nonce_check ) ) { + return $nonce_check; + } + + if ( ! current_user_can( 'edit_posts' ) ) { + return new \WP_Error( + 'airowp_template_forbidden', + __( 'You do not have permission to import templates.', 'airo-wp' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Verifies the REST nonce header to block CSRF against these routes. + * + * Mirrors the project-wide pattern in + * airo-wp\Blocks\Query\Controller::check_permission(). Editor callers + * use @wordpress/api-fetch, which sends the X-WP-Nonce header. + * + * @param \WP_REST_Request $request Incoming REST request. + * @return true|\WP_Error + */ + private static function verify_nonce( \WP_REST_Request $request ) { + $nonce = $request->get_header( 'X-WP-Nonce' ); + if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { + return new \WP_Error( + 'airowp_template_forbidden', + __( 'Invalid nonce.', 'airo-wp' ), + array( 'status' => 401 ) + ); + } + + return true; + } + + /** + * Handles GET /airo-wp/v1/query/template. + * + * Walks the post content of the requested post, finds the airo-wp/query + * block whose `queryId` attribute matches `query_id`, and returns a JSON + * export blob. + * + * @param \WP_REST_Request $request Incoming REST request. + * @return \WP_REST_Response|\WP_Error + */ + public static function export( \WP_REST_Request $request ) { + $post_id = (int) $request['post_id']; + $query_id = (string) $request['query_id']; + + $post = get_post( $post_id ); + if ( ! $post ) { + return new \WP_Error( + 'airowp_template_not_found', + __( 'Post not found.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + $blocks = parse_blocks( $post->post_content ); + $block = self::find_query_block( $blocks, $query_id ); + + if ( null === $block ) { + return new \WP_Error( + 'airowp_template_block_not_found', + __( 'No airo-wp/query block with that queryId was found in the post.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + // Serialize inner blocks back to block-comment markup so the import + // path can round-trip through parse_blocks() cleanly. + $inner_html = self::serialize_inner_blocks( $block ); + + return rest_ensure_response( + array( + 'schemaVersion' => self::SCHEMA_VERSION, + 'exportedAt' => gmdate( 'Y-m-d\TH:i:s\Z' ), + 'blockName' => 'airo-wp/query', + 'attributes' => $block['attrs'], + 'innerBlocks' => $inner_html, + ) + ); + } + + /** + * Handles POST /airo-wp/v1/query/template. + * + * Validates the incoming blob, filters attributes to the current block.json + * allowlist, generates a fresh queryId, and returns serialized block markup + * ready to paste into the editor. + * + * @param \WP_REST_Request $request Incoming REST request. + * @return \WP_REST_Response|\WP_Error + */ + public static function import( \WP_REST_Request $request ) { + $schema_version = (int) $request['schemaVersion']; + $block_name = (string) $request['blockName']; + $raw_attrs = (array) $request['attributes']; + $inner_html = (string) $request['innerBlocks']; + + if ( self::SCHEMA_VERSION !== $schema_version ) { + return new \WP_Error( + 'airowp_template_schema_mismatch', + sprintf( + /* translators: 1: provided schema version, 2: expected version */ + __( 'Unsupported schemaVersion %1$d. Expected %2$d.', 'airo-wp' ), + $schema_version, + self::SCHEMA_VERSION + ), + array( 'status' => 400 ) + ); + } + + if ( 'airo-wp/query' !== $block_name ) { + return new \WP_Error( + 'airowp_template_wrong_block', + __( 'blockName must be "airo-wp/query".', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + // Filter to allowed attribute keys only; drop anything not in block.json. + $allowed_keys = self::allowed_attribute_keys(); + $attrs = array(); + foreach ( $allowed_keys as $key ) { + if ( array_key_exists( $key, $raw_attrs ) ) { + $attrs[ $key ] = $raw_attrs[ $key ]; + } + } + + // Generate a fresh queryId so the imported block does not collide with + // sibling blocks that are already bound to the exported queryId. + $attrs['queryId'] = self::generate_query_id(); + + // Build block comment markup using serialize_block_attributes() so that + // attribute values containing "-->" are escaped as "\u002d\u002d>" + // and cannot prematurely close the block comment. + $attrs_str = serialize_block_attributes( $attrs ); + $markup = '' . "\n" + . $inner_html . "\n" + . ''; + + return rest_ensure_response( + array( + 'blockMarkup' => $markup, + ) + ); + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + /** + * Recursively searches a parsed block tree for the first airo-wp/query + * block whose `queryId` attribute matches $query_id. + * + * @param array $blocks Parsed block tree from parse_blocks(). + * @param string $query_id The queryId to find. + * @return array|null The matched block array, or null if not found. + */ + private static function find_query_block( array $blocks, $query_id ) { + foreach ( $blocks as $block ) { + if ( + ! empty( $block['blockName'] ) + && 'airo-wp/query' === $block['blockName'] + && ( $block['attrs']['queryId'] ?? '' ) === $query_id + ) { + return $block; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $found = self::find_query_block( $block['innerBlocks'], $query_id ); + if ( null !== $found ) { + return $found; + } + } + } + + return null; + } + + /** + * Serializes only the inner blocks of a parsed block back to block-comment + * markup, excluding the outer block's own opening/closing comments. + * + * For export we want `innerBlocks` to contain the template markup (the + * item template + sibling blocks), not the query block itself, so that + * import can assemble a fresh outer wrapper with new attributes. + * + * @param array $block A single parsed block array. + * @return string Serialized block-comment markup of the inner blocks. + */ + private static function serialize_inner_blocks( array $block ) { + if ( empty( $block['innerBlocks'] ) ) { + return ''; + } + + return self::serialize_blocks_markup( $block['innerBlocks'] ); + } + + /** + * Serializes a parsed block list back to block-comment markup. + * + * @param array $blocks Parsed blocks from parse_blocks(). + * @return string + */ + private static function serialize_blocks_markup( array $blocks ) { + $parts = array(); + foreach ( $blocks as $block ) { + $parts[] = serialize_block( $block ); + } + + return implode( "\n", $parts ); + } + + /** + * Returns the list of attribute keys that are accepted on import. + * + * Reads from WP_Block_Type_Registry when the block has been registered + * (normal request path). Falls back to a hardcoded list from block.json + * if called before block registration (e.g. some unit-test bootstraps). + * + * @return string[] + */ + private static function allowed_attribute_keys() { + $block_type = \WP_Block_Type_Registry::get_instance()->get_registered( 'airo-wp/query' ); + + if ( $block_type && is_array( $block_type->attributes ) ) { + return array_keys( $block_type->attributes ); + } + + // Fallback: keys from src/blocks/query/block.json as of v2.4. + return array( + 'queryId', + 'source', + 'relationshipField', + 'relationshipFallback', + 'postType', + 'perPage', + 'offset', + 'orderBy', + 'orderByMetaKey', + 'order', + 'search', + 'bindSearchTo', + 'author', + 'excludeCurrent', + 'ignoreSticky', + 'manualIds', + 'taxQuery', + 'metaQuery', + 'tagName', + 'itemTagName', + 'columns', + 'columnsTablet', + 'columnsMobile', + 'columnGap', + 'showPlaceholder', + 'emitSchema', + 'groupBy', + ); + } + + /** + * Generates a compact, unique queryId for imported blocks. + * + * Format: `q-` followed by 10 hex characters (no hyphens). + * Example: `q-a3f2c1b4d9` + * + * @return string + */ + private static function generate_query_id() { + return 'q-' . substr( str_replace( '-', '', wp_generate_uuid4() ), 0, 10 ); + } +} diff --git a/includes/Blocks/Package.php b/includes/Blocks/Package.php new file mode 100644 index 0000000..5763997 --- /dev/null +++ b/includes/Blocks/Package.php @@ -0,0 +1,77 @@ + ucfirst( basename( $cat_dir ) ) ) ); + + foreach ( (array) glob( $cat_dir . '/*.php' ) as $pattern_file ) { + // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + $pattern = require $pattern_file; + $name = 'airo-wp/' . basename( $cat_dir ) . '/' . basename( $pattern_file, '.php' ); + register_block_pattern( $name, $pattern ); + } + } + } +} diff --git a/includes/Container.php b/includes/Container.php new file mode 100644 index 0000000..3f069a0 --- /dev/null +++ b/includes/Container.php @@ -0,0 +1,62 @@ +container = $runtime_container ?? new RuntimeContainer( + array( + self::class => $this, + ContainerInterface::class => $this, + ) + ); + } + + /** + * Get a service from the container. + * + * @param string $id Class name. + * @return mixed + */ + public function get( $id ) { + return $this->container->get( $id ); + } + + /** + * Check if the container has a service. + * + * @param string $id Class name. + * @return bool + */ + public function has( $id ): bool { + return $this->container->has( $id ); + } +} diff --git a/includes/Internal/DependencyManagement/ContainerException.php b/includes/Internal/DependencyManagement/ContainerException.php new file mode 100644 index 0000000..6010f3b --- /dev/null +++ b/includes/Internal/DependencyManagement/ContainerException.php @@ -0,0 +1,19 @@ + + */ + protected $resolved_cache; + + /** + * Initial resolved instances passed to the constructor. + * + * @var array + */ + protected $initial_resolved_cache; + + /** + * Constructor. + * + * @param array $initial_resolved_cache Pre-resolved instances. + */ + public function __construct( array $initial_resolved_cache ) { + $this->initial_resolved_cache = $initial_resolved_cache; + $this->resolved_cache = $initial_resolved_cache; + } + + /** + * Get a service instance. + * + * @param string $class_name Class to resolve. + * @return object + * @throws ContainerException When resolution fails. + */ + public function get( string $class_name ) { + $class_name = trim( $class_name, '\\' ); + $resolve_chain = array(); + return $this->get_core( $class_name, $resolve_chain ); + } + + /** + * Check if a class can be resolved. + * + * @param string $class_name Class name. + * @return bool + */ + public function has( string $class_name ): bool { + $class_name = trim( $class_name, '\\' ); + return isset( $this->resolved_cache[ $class_name ] ) + || ( $this->is_class_allowed( $class_name ) && class_exists( $class_name ) ); + } + + /** + * Core resolution logic. + * + * @param string $class_name Class to resolve. + * @param string[] $resolve_chain Active resolution chain. + * @return object + * @throws ContainerException When resolution fails. + */ + protected function get_core( string $class_name, array &$resolve_chain ) { + if ( isset( $this->resolved_cache[ $class_name ] ) ) { + return $this->resolved_cache[ $class_name ]; + } + + if ( in_array( $class_name, $resolve_chain, true ) ) { + throw new ContainerException( + 'Recursive resolution of class \'' . esc_html( $class_name ) . '\'. Chain: ' . esc_html( implode( ', ', $resolve_chain ) ) + ); + } + + if ( ! $this->is_class_allowed( $class_name ) ) { + throw new ContainerException( + 'Class \'' . esc_html( $class_name ) . '\' is outside ' . esc_html( self::ROOT_NAMESPACE ) + ); + } + + if ( ! class_exists( $class_name ) ) { + throw new ContainerException( 'Class \'' . esc_html( $class_name ) . '\' does not exist.' ); + } + + $resolve_chain[] = $class_name; + + try { + $instance = $this->instantiate( $class_name, $resolve_chain ); + } catch ( ReflectionException $e ) { + throw new ContainerException( + 'Reflection error resolving \'' . esc_html( $class_name ) . '\': ' . esc_html( $e->getMessage() ), + 0, + $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- previous exception, not output. + ); + } + + $this->resolved_cache[ $class_name ] = $instance; + return $instance; + } + + /** + * Whether the class is in the allowed namespace. + * + * @param string $class_name Class name. + * @return bool + */ + private function is_class_allowed( string $class_name ): bool { + return 0 === strpos( $class_name, self::ROOT_NAMESPACE ) + || isset( $this->initial_resolved_cache[ $class_name ] ); + } + + /** + * Instantiate a class via reflection. + * + * @param string $class_name Class to instantiate. + * @param string[] $resolve_chain Active resolution chain. + * @return object + * @throws ContainerException When instantiation fails. + */ + private function instantiate( string $class_name, array &$resolve_chain ): object { + $ref = new ReflectionClass( $class_name ); + $ctor = $ref->getConstructor(); + + $args = array(); + if ( null !== $ctor && ! $ctor->isPublic() ) { + throw new ContainerException( '\'' . esc_html( $class_name ) . '\' constructor is not public.' ); + } + if ( null !== $ctor ) { + $args = $this->resolve_parameters( $ctor, $resolve_chain ); + } + + $instance = $ref->newInstanceArgs( $args ); + $this->invoke_init( $instance, $resolve_chain ); + return $instance; + } + + /** + * Invoke public init() method if present. + * + * @param object $instance Instance to initialize. + * @param string[] $resolve_chain Active resolution chain. + */ + private function invoke_init( object $instance, array &$resolve_chain ): void { + if ( ! method_exists( $instance, 'init' ) ) { + return; + } + $method = new ReflectionMethod( $instance, 'init' ); + if ( ! $method->isPublic() || $method->isStatic() ) { + return; + } + $args = $this->resolve_parameters( $method, $resolve_chain ); + $method->invokeArgs( $instance, $args ); + } + + /** + * Resolve reflection parameters for a method. + * + * @param ReflectionMethod $reflection_method Method or constructor. + * @param string[] $resolve_chain Active resolution chain. + * @return array + * @throws ContainerException When a parameter cannot be resolved. + */ + private function resolve_parameters( ReflectionMethod $reflection_method, array &$resolve_chain ): array { + $args = array(); + foreach ( $reflection_method->getParameters() as $param ) { + $args[] = $this->resolve_parameter( $param, $resolve_chain ); + } + return $args; + } + + /** + * Resolve a single reflection parameter. + * + * @param ReflectionParameter $param Parameter to resolve. + * @param string[] $resolve_chain Active resolution chain. + * @return mixed + * @throws ContainerException When parameter cannot be resolved. + */ + private function resolve_parameter( ReflectionParameter $param, array &$resolve_chain ) { + $type = $param->getType(); + if ( ! $type instanceof ReflectionNamedType || $type->isBuiltin() ) { + if ( $param->isDefaultValueAvailable() ) { + return $param->getDefaultValue(); + } + throw new ContainerException( + 'Cannot resolve parameter $' . esc_html( $param->getName() ) . ' without type hint.' + ); + } + return $this->get_core( $type->getName(), $resolve_chain ); + } +} diff --git a/includes/Internal/DependencyManagement/TestingContainer.php b/includes/Internal/DependencyManagement/TestingContainer.php new file mode 100644 index 0000000..05126b2 --- /dev/null +++ b/includes/Internal/DependencyManagement/TestingContainer.php @@ -0,0 +1,36 @@ +resolved_cache[ $class_name ] = $instance; + } + + /** + * Reset resolved instances to initial cache. + */ + public function reset(): void { + $this->resolved_cache = $this->initial_resolved_cache; + } +} diff --git a/includes/Internal/Testing/Fixtures/SampleDependency.php b/includes/Internal/Testing/Fixtures/SampleDependency.php new file mode 100644 index 0000000..bc371da --- /dev/null +++ b/includes/Internal/Testing/Fixtures/SampleDependency.php @@ -0,0 +1,15 @@ +dependency = $dependency; + } +} diff --git a/includes/Internal/Testing/Fixtures/SampleServiceWithInit.php b/includes/Internal/Testing/Fixtures/SampleServiceWithInit.php new file mode 100644 index 0000000..d467083 --- /dev/null +++ b/includes/Internal/Testing/Fixtures/SampleServiceWithInit.php @@ -0,0 +1,26 @@ +initialized = ( $dependency instanceof SampleDependency ); + } +} diff --git a/includes/Mcp/Infrastructure/AbilitiesApiProxy.php b/includes/Mcp/Infrastructure/AbilitiesApiProxy.php new file mode 100644 index 0000000..f33d0a1 --- /dev/null +++ b/includes/Mcp/Infrastructure/AbilitiesApiProxy.php @@ -0,0 +1,322 @@ + + */ + private array $tools; + + /** + * Constructor. + * + * @param SiteInfo $site_info Site info tool. + * @param UpdateSiteOptions $update_site_options Update site options tool. + * @param ActivatePlugin $activate_plugin Activate plugin tool. + * @param DeactivatePlugin $deactivate_plugin Deactivate plugin tool. + * @param GetPlugin $get_plugin Get plugin tool. + * @param ListPlugins $list_plugins List plugins tool. + * @param UpdatePlugin $update_plugin Update plugin tool. + * @param ActivateTheme $activate_theme Activate theme tool. + * @param GetThemes $get_themes Get themes tool. + * @param SwitchTheme $switch_theme Switch theme tool. + * @param CreatePost $create_post Create post tool. + * @param UpdatePost $update_post Update post tool. + * @param DeletePost $delete_post Delete post tool. + * @param ListPosts $list_posts List posts tool. + * @param GetPost $get_post Get post tool. + * @param GetPostByOptionName $get_post_by_option_name Get post by option name tool. + * @param ListPostRevisions $list_post_revisions List post revisions tool. + * @param RestorePostRevision $restore_post_revision Restore post revision tool. + * @param UpdatePostImageAltText $update_post_image_alt_text Update post image alt text tool. + * @param CreatePageDraft $create_page_draft Create page draft tool. + * @param PublishPageDraft $publish_page_draft Publish page draft tool. + * @param DiscardPageDraft $discard_page_draft Discard page draft tool. + * @param GetPageDraftStatus $get_page_draft_status Get page draft status tool. + * @param ListPageRevisions $list_page_revisions List page revisions tool. + * @param GetPageRevision $get_page_revision Get page revision tool. + * @param DeletePageRevision $delete_page_revision Delete page revision tool. + * @param DeleteMedia $delete_media Delete media tool. + * @param GetAllMedia $get_all_media Get all media tool. + * @param GetMediaById $get_media_by_id Get media by ID tool. + * @param ListMedia $list_media List media tool. + * @param UpdateMediaMeta $update_media_meta Update media meta tool. + * @param UploadImage $upload_image Upload image tool. + * @param ListTemplates $list_templates List templates tool. + * @param UpdateTemplate $update_template Update template tool. + * @param DeleteTemplate $delete_template Delete template tool. + * @param ListTemplateParts $list_template_parts List template parts tool. + * @param UpdateTemplatePart $update_template_part Update template part tool. + * @param DeleteTemplatePart $delete_template_part Delete template part tool. + * @param ListTemplateRevisions $list_template_revisions List template revisions tool. + * @param ListTemplatePartRevisions $list_template_part_revisions List template part revisions tool. + * @param GetGlobalStyles $get_global_styles Get global styles tool. + * @param ListGlobalStyles $list_global_styles List global styles tool. + * @param ListGlobalStylesRevisions $list_global_styles_revisions List global styles revisions tool. + * @param UpdateGlobalStyles $update_global_styles Update global styles tool. + * @param GetBlockTypes $get_block_types Get block types tool. + * @param GetBlockPatterns $get_block_patterns Get block patterns tool. + * @param ListNavigations $list_navigations List navigations tool. + * @param GetNavigation $get_navigation Get navigation tool. + * @param CreateNavigation $create_navigation Create navigation tool. + * @param UpdateNavigation $update_navigation Update navigation tool. + * @param DeleteNavigation $delete_navigation Delete navigation tool. + * @param ListNavigationRevisions $list_navigation_revisions List navigation revisions tool. + */ + public function __construct( + SiteInfo $site_info, + UpdateSiteOptions $update_site_options, + ActivatePlugin $activate_plugin, + DeactivatePlugin $deactivate_plugin, + GetPlugin $get_plugin, + ListPlugins $list_plugins, + UpdatePlugin $update_plugin, + ActivateTheme $activate_theme, + GetThemes $get_themes, + SwitchTheme $switch_theme, + CreatePost $create_post, + UpdatePost $update_post, + DeletePost $delete_post, + ListPosts $list_posts, + GetPost $get_post, + GetPostByOptionName $get_post_by_option_name, + ListPostRevisions $list_post_revisions, + RestorePostRevision $restore_post_revision, + UpdatePostImageAltText $update_post_image_alt_text, + CreatePageDraft $create_page_draft, + PublishPageDraft $publish_page_draft, + DiscardPageDraft $discard_page_draft, + GetPageDraftStatus $get_page_draft_status, + ListPageRevisions $list_page_revisions, + GetPageRevision $get_page_revision, + DeletePageRevision $delete_page_revision, + DeleteMedia $delete_media, + GetAllMedia $get_all_media, + GetMediaById $get_media_by_id, + ListMedia $list_media, + UpdateMediaMeta $update_media_meta, + UploadImage $upload_image, + ListTemplates $list_templates, + UpdateTemplate $update_template, + DeleteTemplate $delete_template, + ListTemplateParts $list_template_parts, + UpdateTemplatePart $update_template_part, + DeleteTemplatePart $delete_template_part, + ListTemplateRevisions $list_template_revisions, + ListTemplatePartRevisions $list_template_part_revisions, + GetGlobalStyles $get_global_styles, + ListGlobalStyles $list_global_styles, + ListGlobalStylesRevisions $list_global_styles_revisions, + UpdateGlobalStyles $update_global_styles, + GetBlockTypes $get_block_types, + GetBlockPatterns $get_block_patterns, + ListNavigations $list_navigations, + GetNavigation $get_navigation, + CreateNavigation $create_navigation, + UpdateNavigation $update_navigation, + DeleteNavigation $delete_navigation, + ListNavigationRevisions $list_navigation_revisions + ) { + $this->tools = array( + $site_info, + $update_site_options, + $activate_plugin, + $deactivate_plugin, + $get_plugin, + $list_plugins, + $update_plugin, + $activate_theme, + $get_themes, + $switch_theme, + $create_post, + $update_post, + $delete_post, + $list_posts, + $get_post, + $get_post_by_option_name, + $list_post_revisions, + $restore_post_revision, + $update_post_image_alt_text, + $create_page_draft, + $publish_page_draft, + $discard_page_draft, + $get_page_draft_status, + $list_page_revisions, + $get_page_revision, + $delete_page_revision, + $delete_media, + $get_all_media, + $get_media_by_id, + $list_media, + $update_media_meta, + $upload_image, + $list_templates, + $update_template, + $delete_template, + $list_template_parts, + $update_template_part, + $delete_template_part, + $list_template_revisions, + $list_template_part_revisions, + $get_global_styles, + $list_global_styles, + $list_global_styles_revisions, + $update_global_styles, + $get_block_types, + $get_block_patterns, + $list_navigations, + $get_navigation, + $create_navigation, + $update_navigation, + $delete_navigation, + $list_navigation_revisions, + ); + } + + /** + * Register hooks for both Abilities API variants. + */ + public function setup(): void { + add_action( 'wp_abilities_api_categories_init', array( $this, 'register_categories' ) ); + add_action( 'wp_abilities_api_init', array( $this, 'register_tools' ) ); + add_action( 'abilities_api_init', array( $this, 'register_tools' ) ); + } + + /** + * Register ability categories required by our tools. + * + * The wp_register_ability_category() function is a WP 6.9+ core function. On WP 6.8 the + * Abilities API is loaded as a standalone package that provides the same + * function. The function_exists() guard ensures graceful degradation when + * neither source is available. + */ + public function register_categories(): void { + if ( ! function_exists( 'wp_register_ability_category' ) ) { + return; + } + + wp_register_ability_category( + 'site-management', + array( + 'label' => __( 'Site Management', 'airo-wp' ), + 'description' => __( 'Abilities for managing the WordPress site.', 'airo-wp' ), + ) + ); + + wp_register_ability_category( + 'plugin-management', + array( + 'label' => __( 'Plugin Management', 'airo-wp' ), + 'description' => __( 'Abilities for managing WordPress plugins.', 'airo-wp' ), + ) + ); + + wp_register_ability_category( + 'theme-management', + array( + 'label' => __( 'Theme Management', 'airo-wp' ), + 'description' => __( 'Abilities for managing WordPress themes.', 'airo-wp' ), + ) + ); + + wp_register_ability_category( + 'content-management', + array( + 'label' => __( 'Content Management', 'airo-wp' ), + 'description' => __( 'Abilities for managing WordPress content.', 'airo-wp' ), + ) + ); + + wp_register_ability_category( + 'media-management', + array( + 'label' => __( 'Media Management', 'airo-wp' ), + 'description' => __( 'Abilities for managing WordPress media.', 'airo-wp' ), + ) + ); + } + + /** + * Call register() on each tool. + */ + public function register_tools(): void { + foreach ( $this->tools as $tool ) { + $tool->register(); + } + } +} diff --git a/includes/Mcp/Package.php b/includes/Mcp/Package.php new file mode 100644 index 0000000..d64d885 --- /dev/null +++ b/includes/Mcp/Package.php @@ -0,0 +1,182 @@ +get( AbilitiesApiProxy::class ); + $proxy->setup(); + + $draft_service = $container->get( DraftPageService::class ); + add_action( 'before_delete_post', array( $draft_service, 'cleanup_draft_meta' ) ); + + // Initialise own McpAdapter instance. Internally hooks rest_api_init:15 + // to fire mcp_adapter_init for REST requests only. + $adapter = McpAdapter::instance(); + + // Create the 'airo-wp' MCP server when OUR adapter fires mcp_adapter_init. + // The identity check prevents responding to mcp-adapter-initializer's adapter + // if both plugins are active (they share the same action name but are separate + // Strauss-prefixed instances). + add_action( + 'mcp_adapter_init', + static function ( $fired_adapter ) use ( $adapter ) { + if ( $fired_adapter !== $adapter ) { + return; + } + $adapter->create_server( + 'airo-wp', + 'airo-wp/v1', + 'mcp/streamable', + __( 'Airo WP MCP Server', 'airo-wp' ), + __( 'MCP server for Airo WP tools.', 'airo-wp' ), + AIRO_WP_VERSION, + array( HttpTransport::class ), + null, // Error handler: NullMcpErrorHandler used by default. + null, // Observability handler: NullMcpObservabilityHandler used by default. + array( + SiteInfo::TOOL_ID, + UpdateSiteOptions::TOOL_ID, + ActivatePlugin::TOOL_ID, + DeactivatePlugin::TOOL_ID, + GetPlugin::TOOL_ID, + ListPlugins::TOOL_ID, + UpdatePlugin::TOOL_ID, + ActivateTheme::TOOL_ID, + GetThemes::TOOL_ID, + SwitchTheme::TOOL_ID, + CreatePost::TOOL_ID, + UpdatePost::TOOL_ID, + DeletePost::TOOL_ID, + ListPosts::TOOL_ID, + GetPost::TOOL_ID, + GetPostByOptionName::TOOL_ID, + ListPostRevisions::TOOL_ID, + RestorePostRevision::TOOL_ID, + UpdatePostImageAltText::TOOL_ID, + CreatePageDraft::TOOL_ID, + PublishPageDraft::TOOL_ID, + DiscardPageDraft::TOOL_ID, + GetPageDraftStatus::TOOL_ID, + ListPageRevisions::TOOL_ID, + GetPageRevision::TOOL_ID, + DeletePageRevision::TOOL_ID, + DeleteMedia::TOOL_ID, + GetAllMedia::TOOL_ID, + GetMediaById::TOOL_ID, + ListMedia::TOOL_ID, + UpdateMediaMeta::TOOL_ID, + UploadImage::TOOL_ID, + ListTemplates::TOOL_ID, + UpdateTemplate::TOOL_ID, + DeleteTemplate::TOOL_ID, + ListTemplateParts::TOOL_ID, + UpdateTemplatePart::TOOL_ID, + DeleteTemplatePart::TOOL_ID, + ListTemplateRevisions::TOOL_ID, + ListTemplatePartRevisions::TOOL_ID, + GetGlobalStyles::TOOL_ID, + ListGlobalStyles::TOOL_ID, + ListGlobalStylesRevisions::TOOL_ID, + UpdateGlobalStyles::TOOL_ID, + GetBlockTypes::TOOL_ID, + GetBlockPatterns::TOOL_ID, + ListNavigations::TOOL_ID, + GetNavigation::TOOL_ID, + CreateNavigation::TOOL_ID, + UpdateNavigation::TOOL_ID, + DeleteNavigation::TOOL_ID, + ListNavigationRevisions::TOOL_ID, + ), + array(), // Resources. + array(), // Prompts. + 'is_user_logged_in' + ); + } + ); + } +} diff --git a/includes/Mcp/Services/PluginHelper.php b/includes/Mcp/Services/PluginHelper.php new file mode 100644 index 0000000..45d0c0a --- /dev/null +++ b/includes/Mcp/Services/PluginHelper.php @@ -0,0 +1,141 @@ +load_admin_file( 'plugin.php' ); + } + + $all_plugins = get_plugins(); + + $common_path = $plugin_slug . '/' . $plugin_slug . '.php'; + if ( isset( $all_plugins[ $common_path ] ) ) { + return $common_path; + } + + foreach ( array_keys( $all_plugins ) as $plugin_file ) { + if ( dirname( $plugin_file ) === $plugin_slug ) { + return $plugin_file; + } + } + + return false; + } + + /** + * Optionally refresh the `update_plugins` site transient. + * + * The transient is normally kept fresh by core's cron schedule. Pass true + * when the caller explicitly requested up-to-the-second update data. + * + * @param bool $force Whether to force a refresh by calling wp_update_plugins(). + * + * @return void + */ + public function refresh_updates( bool $force = false ): void { + if ( ! $force ) { + return; + } + + if ( ! function_exists( 'wp_update_plugins' ) ) { + $this->load_admin_file( 'update.php' ); + } + + wp_update_plugins(); + } + + /** + * Get update availability info for an installed plugin. + * + * Reads the `update_plugins` site transient maintained by core. Callers + * that need fresh data should invoke {@see refresh_updates()} first — on + * headless installs the transient can sit missing for hours since WP cron + * only fires on incoming requests, so "no update" here may just mean nothing + * has called `wp_update_plugins()` since the cache last expired. + * + * @param string $plugin_file Plugin file path relative to WP_PLUGIN_DIR (e.g. "akismet/akismet.php"). + * + * @return array{update_available: bool, new_version: string|null} + */ + public function get_update_info( string $plugin_file ): array { + $no_update = array( + 'update_available' => false, + 'new_version' => null, + ); + + $updates = get_site_transient( 'update_plugins' ); + + if ( ! is_object( $updates ) || ! isset( $updates->response ) || ! is_array( $updates->response ) ) { + return $no_update; + } + + if ( ! isset( $updates->response[ $plugin_file ] ) ) { + return $no_update; + } + + $response = $updates->response[ $plugin_file ]; + + if ( ! is_object( $response ) ) { + return $no_update; + } + + $new_version = isset( $response->new_version ) ? $response->new_version : null; + + if ( empty( $new_version ) ) { + return $no_update; + } + + return array( + 'update_available' => true, + 'new_version' => $new_version, + ); + } + + /** + * Load a wp-admin/includes file. + * + * In tests (PHPUnit), functions are pre-defined in bootstrap.php, so skip loading. + * + * @param string $filename Admin filename (e.g., 'plugin.php'). + * + * @return void + */ + private function load_admin_file( string $filename ): void { + if ( defined( 'PHPUNIT_RUNNING' ) ) { + return; + } + + $full_path = ABSPATH . 'wp-admin/includes/' . $filename; + + if ( file_exists( $full_path ) ) { + // @codeCoverageIgnoreStart + require_once $full_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + // @codeCoverageIgnoreEnd + } + } +} diff --git a/includes/Mcp/Tools/BaseTool.php b/includes/Mcp/Tools/BaseTool.php new file mode 100644 index 0000000..11f312b --- /dev/null +++ b/includes/Mcp/Tools/BaseTool.php @@ -0,0 +1,94 @@ + $input Input parameters. + * @return array Execution result. + */ + abstract public function execute( array $input ): array; + + /** + * Register this tool as a WordPress ability. + */ + abstract public function register(): void; + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + abstract public function check_permissions(): bool; + + /** + * Load WordPress admin file. + * + * @param string $filename Admin filename (e.g., 'plugin.php'). + */ + protected function load_admin_file( string $filename ): void { + if ( defined( 'PHPUNIT_RUNNING' ) ) { + return; + } + + $full_path = ABSPATH . 'wp-admin/includes/' . $filename; + + if ( file_exists( $full_path ) ) { + require_once $full_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- path is constructed from ABSPATH, a trusted constant. + } + } + + /** + * Build a standard output schema with success/message base fields. + * + * @param string $description Additional description. + * @param array $properties Additional properties. + * @param array $required Additional required properties. + * @return array Output schema. + */ + protected function build_output_schema( string $description = '', array $properties = array(), array $required = array() ): array { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'success' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the operation succeeded', 'airo-wp' ), + ), + 'message' => array( + 'type' => 'string', + 'description' => __( 'Success or error message', 'airo-wp' ), + ), + ), + 'required' => array( 'success' ), + ); + + if ( ! empty( $description ) ) { + $schema['description'] = $description; + } + + if ( ! empty( $properties ) ) { + $schema['properties'] = array_merge( $schema['properties'], $properties ); + } + + if ( ! empty( $required ) ) { + $schema['required'] = array_merge( $schema['required'], $required ); + } + + return $schema; + } +} diff --git a/includes/Mcp/Tools/GlobalStyles/GetBlockPatterns.php b/includes/Mcp/Tools/GlobalStyles/GetBlockPatterns.php new file mode 100644 index 0000000..be18427 --- /dev/null +++ b/includes/Mcp/Tools/GlobalStyles/GetBlockPatterns.php @@ -0,0 +1,162 @@ + __( 'Get Block Patterns', 'airo-wp' ), + 'description' => __( 'Retrieves all registered block patterns on the site', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the get block patterns tool. + * + * @param array $_input Input parameters (none required). + * @return array Block patterns result or error. + */ + public function execute( array $_input ): array { + if ( ! class_exists( 'WP_Block_Patterns_Registry' ) ) { + return array( + 'success' => false, + 'message' => __( 'Block patterns registry is not available', 'airo-wp' ), + ); + } + + $registry = \WP_Block_Patterns_Registry::get_instance(); + $registered_patterns = $registry->get_all_registered(); + + $block_patterns = array(); + + foreach ( $registered_patterns as $pattern_data ) { + $block_patterns[] = array( + 'name' => $pattern_data['name'] ?? '', + 'title' => $pattern_data['title'] ?? '', + 'description' => $pattern_data['description'] ?? '', + 'content' => $pattern_data['content'] ?? '', + 'categories' => $pattern_data['categories'] ?? array(), + 'keywords' => $pattern_data['keywords'] ?? array(), + 'blockTypes' => $pattern_data['blockTypes'] ?? array(), + ); + } + + return array( + 'success' => true, + 'block_patterns' => $block_patterns, + // translators: %d is the number of block patterns found. + 'message' => sprintf( __( 'Retrieved %d registered block patterns', 'airo-wp' ), count( $block_patterns ) ), + ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array(), + 'required' => array(), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Block patterns retrieval result', 'airo-wp' ), + array( + 'block_patterns' => array( + 'type' => 'array', + 'description' => __( 'Array of registered block patterns', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'The pattern name', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The pattern title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The pattern description', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The pattern HTML content', 'airo-wp' ), + ), + 'categories' => array( + 'type' => 'array', + 'description' => __( 'Array of pattern categories', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'keywords' => array( + 'type' => 'array', + 'description' => __( 'Array of pattern keywords', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'blockTypes' => array( + 'type' => 'array', + 'description' => __( 'Array of supported block types', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/GlobalStyles/GetBlockTypes.php b/includes/Mcp/Tools/GlobalStyles/GetBlockTypes.php new file mode 100644 index 0000000..b993e20 --- /dev/null +++ b/includes/Mcp/Tools/GlobalStyles/GetBlockTypes.php @@ -0,0 +1,218 @@ + __( 'Get Block Types', 'airo-wp' ), + 'description' => __( 'Retrieves all registered block types on the site', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the get block types tool. + * + * @param array $_input Input parameters (none required). + * @return array Block types result or error. + */ + public function execute( array $_input ): array { + if ( ! class_exists( 'WP_Block_Type_Registry' ) ) { + return array( + 'success' => false, + 'message' => __( 'Block type registry is not available', 'airo-wp' ), + ); + } + + $block_registry = \WP_Block_Type_Registry::get_instance(); + $registered_blocks = $block_registry->get_all_registered(); + + if ( ! is_array( $registered_blocks ) ) { + return array( + 'success' => false, + 'message' => __( 'Unable to retrieve registered block types', 'airo-wp' ), + ); + } + + $block_types = array(); + + foreach ( $registered_blocks as $block_name => $block_type ) { + $block_types[] = array( + 'name' => $block_name, + 'title' => isset( $block_type->title ) ? $block_type->title : '', + 'description' => isset( $block_type->description ) ? $block_type->description : '', + 'category' => isset( $block_type->category ) ? $block_type->category : '', + 'icon' => isset( $block_type->icon ) ? $block_type->icon : '', + 'keywords' => isset( $block_type->keywords ) ? $block_type->keywords : array(), + 'api_version' => isset( $block_type->api_version ) ? (int) $block_type->api_version : 1, + 'is_dynamic' => $block_type->is_dynamic(), + 'parent' => isset( $block_type->parent ) ? $block_type->parent : array(), + 'ancestor' => isset( $block_type->ancestor ) ? $block_type->ancestor : array(), + 'allowed_blocks' => isset( $block_type->allowed_blocks ) ? $block_type->allowed_blocks : array(), + 'attributes' => $block_type->get_attributes(), + 'supports' => isset( $block_type->supports ) ? $block_type->supports : array(), + 'styles' => isset( $block_type->styles ) ? $block_type->styles : array(), + 'variations' => isset( $block_type->variations ) ? $block_type->variations : array(), + ); + } + + return array( + 'success' => true, + 'block_types' => $block_types, + // translators: %d is the number of block types found. + 'message' => sprintf( __( 'Retrieved %d registered block types', 'airo-wp' ), count( $block_types ) ), + ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array(), + 'required' => array(), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Block types retrieval result', 'airo-wp' ), + array( + 'block_types' => array( + 'type' => 'array', + 'description' => __( 'Array of registered block types', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'The block type name (e.g. core/paragraph)', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The block type title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The block type description', 'airo-wp' ), + ), + 'category' => array( + 'type' => 'string', + 'description' => __( 'The block category', 'airo-wp' ), + ), + 'icon' => array( + 'type' => array( 'string', 'object' ), + 'description' => __( 'The block icon (dashicon slug or SVG object)', 'airo-wp' ), + ), + 'keywords' => array( + 'type' => 'array', + 'description' => __( 'Search keywords for block discovery', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'api_version' => array( + 'type' => 'integer', + 'description' => __( 'Block API version', 'airo-wp' ), + ), + 'is_dynamic' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the block has a server-side render callback', 'airo-wp' ), + ), + 'parent' => array( + 'type' => 'array', + 'description' => __( 'Parent block types that can contain this block', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'ancestor' => array( + 'type' => 'array', + 'description' => __( 'Ancestor block types (any level) that must contain this block', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'allowed_blocks' => array( + 'type' => 'array', + 'description' => __( 'Block types allowed as direct children', 'airo-wp' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'attributes' => array( + 'type' => 'object', + 'description' => __( 'Block attribute definitions (name to schema)', 'airo-wp' ), + ), + 'supports' => array( + 'type' => 'object', + 'description' => __( 'Block support settings (color, spacing, typography, etc.)', 'airo-wp' ), + ), + 'styles' => array( + 'type' => 'array', + 'description' => __( 'Registered block style variations', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + ), + ), + 'variations' => array( + 'type' => 'array', + 'description' => __( 'Block variations with preset configurations', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + ), + ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/GlobalStyles/GetGlobalStyles.php b/includes/Mcp/Tools/GlobalStyles/GetGlobalStyles.php new file mode 100644 index 0000000..8f9abc8 --- /dev/null +++ b/includes/Mcp/Tools/GlobalStyles/GetGlobalStyles.php @@ -0,0 +1,214 @@ + __( 'Get Global Styles', 'airo-wp' ), + 'description' => __( 'Retrieves global style configuration by ID', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Tool input parameters. + * @return array + */ + public function execute( array $input ): array { + try { + $style_id = isset( $input['id'] ) ? absint( $input['id'] ) : 0; + $context = isset( $input['context'] ) ? sanitize_text_field( $input['context'] ) : 'view'; + + if ( empty( $style_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Style ID is required', 'airo-wp' ), + ); + } + + $global_style = get_post( $style_id ); + + if ( ! $global_style || 'wp_global_styles' !== $global_style->post_type ) { + return array( + 'success' => false, + 'message' => __( 'Global style not found or invalid ID', 'airo-wp' ), + ); + } + + $style_content = ! empty( $global_style->post_content ) ? json_decode( $global_style->post_content, true ) : array(); + + $response_data = array( + 'id' => (int) $global_style->ID, + 'title' => array( + 'rendered' => $global_style->post_title, + ), + ); + + if ( 'edit' === $context ) { + $response_data['title']['raw'] = $global_style->post_title; + $response_data['status'] = $global_style->post_status; + $response_data['date'] = $global_style->post_date; + $response_data['date_gmt'] = $global_style->post_date_gmt; + $response_data['modified'] = $global_style->post_modified; + $response_data['modified_gmt'] = $global_style->post_modified_gmt; + $response_data['slug'] = $global_style->post_name; + $response_data['settings'] = isset( $style_content['settings'] ) ? $style_content['settings'] : new \stdClass(); + $response_data['styles'] = isset( $style_content['styles'] ) ? $style_content['styles'] : new \stdClass(); + } elseif ( 'embed' !== $context ) { + // View context: standard fields (embed returns only id and title). + $response_data['status'] = $global_style->post_status; + $response_data['date'] = $global_style->post_date; + $response_data['modified'] = $global_style->post_modified; + $response_data['settings'] = isset( $style_content['settings'] ) ? $style_content['settings'] : new \stdClass(); + $response_data['styles'] = isset( $style_content['styles'] ) ? $style_content['styles'] : new \stdClass(); + } + + return array( + 'success' => true, + 'data' => $response_data, + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + /* translators: %s: Error message */ + 'message' => sprintf( __( 'Error retrieving global style: %s', 'airo-wp' ), $e->getMessage() ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'The ID of the global style to retrieve', 'airo-wp' ), + ), + 'context' => array( + 'type' => 'string', + 'enum' => array( 'view', 'edit', 'embed' ), + 'default' => 'view', + 'description' => __( 'The context in which the request is made', 'airo-wp' ), + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Global styles retrieval result', 'airo-wp' ), + array( + 'data' => array( + 'type' => 'object', + 'description' => __( 'Global style data', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The global style ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'object', + 'description' => __( 'The global style title object', 'airo-wp' ), + 'properties' => array( + 'rendered' => array( + 'type' => 'string', + 'description' => __( 'Rendered title', 'airo-wp' ), + ), + 'raw' => array( + 'type' => 'string', + 'description' => __( 'Raw title (edit context only)', 'airo-wp' ), + ), + ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The global style post status', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The global style creation date', 'airo-wp' ), + ), + 'date_gmt' => array( + 'type' => 'string', + 'description' => __( 'The global style creation date in GMT (edit context only)', 'airo-wp' ), + ), + 'modified' => array( + 'type' => 'string', + 'description' => __( 'The global style modification date', 'airo-wp' ), + ), + 'modified_gmt' => array( + 'type' => 'string', + 'description' => __( 'The global style modification date in GMT (edit context only)', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The global style slug (edit context only)', 'airo-wp' ), + ), + 'settings' => array( + 'type' => 'object', + 'description' => __( 'The global style settings object', 'airo-wp' ), + ), + 'styles' => array( + 'type' => 'object', + 'description' => __( 'The global style styles object', 'airo-wp' ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/GlobalStyles/ListGlobalStyles.php b/includes/Mcp/Tools/GlobalStyles/ListGlobalStyles.php new file mode 100644 index 0000000..3c77faf --- /dev/null +++ b/includes/Mcp/Tools/GlobalStyles/ListGlobalStyles.php @@ -0,0 +1,215 @@ + __( 'List Global Styles', 'airo-wp' ), + 'description' => __( 'Retrieves a list of all global styles configurations, optionally filtered by theme', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the list global styles tool. + * + * @param array $input Input parameters. + * @return array Global styles list result or error. + */ + public function execute( array $input ): array { + try { + $theme = isset( $input['theme'] ) ? sanitize_text_field( $input['theme'] ) : ''; + $status = isset( $input['status'] ) ? $input['status'] : 'any'; + + $post_status = 'any' === $status ? array( 'publish', 'draft' ) : $status; + + $query_args = array( + 'post_type' => 'wp_global_styles', + 'post_status' => $post_status, + 'posts_per_page' => -1, + 'orderby' => 'date', + 'order' => 'DESC', + ); + + if ( ! empty( $theme ) ) { + $query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => 'theme', + 'value' => $theme, + 'compare' => '=', + ), + ); + } + + // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_posts_get_posts + $global_styles = get_posts( $query_args ); + + if ( empty( $global_styles ) ) { + $message = ! empty( $theme ) + /* translators: %s: theme slug */ + ? sprintf( __( 'No global styles found for theme: %s', 'airo-wp' ), $theme ) + : __( 'No global styles found', 'airo-wp' ); + + return array( + 'success' => true, + 'styles' => array(), + 'total' => 0, + 'message' => $message, + ); + } + + $styles = array(); + foreach ( $global_styles as $style ) { + $theme_slug = get_post_meta( $style->ID, 'theme', true ); + + if ( empty( $theme ) || $theme_slug === $theme ) { + $styles[] = array( + 'id' => (int) $style->ID, + 'title' => $style->post_title, + 'theme' => $theme_slug ? $theme_slug : '', + 'status' => $style->post_status, + 'date' => $style->post_date, + 'modified' => $style->post_modified, + ); + } + } + + $total = count( $styles ); + + $message = ! empty( $theme ) + /* translators: 1: number of styles, 2: theme slug */ + ? sprintf( __( 'Found %1$d global styles for theme: %2$s', 'airo-wp' ), $total, $theme ) + /* translators: %d: number of styles */ + : sprintf( __( 'Found %d global styles', 'airo-wp' ), $total ); + + return array( + 'success' => true, + 'styles' => $styles, + 'total' => $total, + 'message' => $message, + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error retrieving global styles: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'theme' => array( + 'type' => 'string', + 'description' => __( 'Optional theme slug to filter global styles by specific theme', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Filter by post status (publish, draft, etc.). Defaults to all statuses', 'airo-wp' ), + 'enum' => array( 'publish', 'draft', 'any' ), + 'default' => 'any', + ), + ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Global styles list result', 'airo-wp' ), + array( + 'styles' => array( + 'type' => 'array', + 'description' => __( 'Array of global styles information', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The global styles post ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The global styles title', 'airo-wp' ), + ), + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug this style belongs to', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The post status (publish, draft, etc.)', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The creation date', 'airo-wp' ), + ), + 'modified' => array( + 'type' => 'string', + 'description' => __( 'The last modified date', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of global styles found', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/GlobalStyles/ListGlobalStylesRevisions.php b/includes/Mcp/Tools/GlobalStyles/ListGlobalStylesRevisions.php new file mode 100644 index 0000000..846cb3e --- /dev/null +++ b/includes/Mcp/Tools/GlobalStyles/ListGlobalStylesRevisions.php @@ -0,0 +1,234 @@ + __( 'List Global Styles Revisions', 'airo-wp' ), + 'description' => __( 'Retrieves revisions for a wp_global_styles post', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the list global styles revisions tool. + * + * @param array $input Input parameters. + * @return array List of revisions or error. + */ + public function execute( array $input ): array { + try { + $global_styles_id = isset( $input['id'] ) ? (int) $input['id'] : 0; + + if ( $global_styles_id <= 0 ) { + return array( + 'success' => false, + 'message' => __( 'Global styles post ID is required', 'airo-wp' ), + ); + } + + $parent_post = get_post( $global_styles_id ); + + if ( ! $parent_post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post "%d" not found', 'airo-wp' ), + $global_styles_id + ), + ); + } + + if ( 'wp_global_styles' !== $parent_post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: 1: post ID, 2: post type */ + __( 'Post "%1$d" is not a wp_global_styles post (got "%2$s")', 'airo-wp' ), + $global_styles_id, + $parent_post->post_type + ), + ); + } + + $request = new \WP_REST_Request( 'GET', '/wp/v2/global-styles/' . $global_styles_id . '/revisions' ); + $request->set_param( 'parent', $global_styles_id ); + $request->set_param( 'per_page', isset( $input['per_page'] ) ? (int) $input['per_page'] : 10 ); + $request->set_param( 'page', isset( $input['page'] ) ? (int) $input['page'] : 1 ); + + if ( ! class_exists( '\WP_REST_Global_Styles_Revisions_Controller' ) ) { + return array( + 'success' => false, + 'message' => __( 'Global styles revisions are not supported on this WordPress version', 'airo-wp' ), + ); + } + + $controller = new \WP_REST_Global_Styles_Revisions_Controller( 'wp_global_styles' ); + $response = $controller->get_items( $request ); + + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $revisions = $response->get_data(); + $headers = $response->get_headers(); + $total = isset( $headers['X-WP-Total'] ) ? (int) $headers['X-WP-Total'] : count( $revisions ); + $total_pages = isset( $headers['X-WP-TotalPages'] ) ? (int) $headers['X-WP-TotalPages'] : 1; + + return array( + 'success' => true, + 'revisions' => $revisions, + 'total' => $total, + 'total_pages' => $total_pages, + 'message' => sprintf( + /* translators: 1: number of revisions, 2: post ID */ + __( 'Retrieved %1$d revision(s) for global styles post "%2$d"', 'airo-wp' ), + count( $revisions ), + $global_styles_id + ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error listing global styles revisions: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of a wp_global_styles post (obtained from list-global-styles or get-global-styles)', 'airo-wp' ), + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to return', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Global styles revisions list result', 'airo-wp' ), + array( + 'revisions' => array( + 'type' => 'array', + 'description' => __( 'Array of revision objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Revision ID', 'airo-wp' ), + ), + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'Parent wp_global_styles post ID', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Author ID', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'Revision date', 'airo-wp' ), + ), + 'styles' => array( + 'type' => 'object', + 'description' => __( 'Decoded styles object', 'airo-wp' ), + ), + 'settings' => array( + 'type' => 'object', + 'description' => __( 'Decoded settings object', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of revisions', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/GlobalStyles/UpdateGlobalStyles.php b/includes/Mcp/Tools/GlobalStyles/UpdateGlobalStyles.php new file mode 100644 index 0000000..bd88e59 --- /dev/null +++ b/includes/Mcp/Tools/GlobalStyles/UpdateGlobalStyles.php @@ -0,0 +1,323 @@ +font_downloader = $font_downloader; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'edit_theme_options' ); + } + + /** + * Register the update global styles ability. + * + * @return void + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Update Global Styles', 'airo-wp' ), + 'description' => __( 'Updates global styles configuration including styles, settings, and title', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Tool input parameters. + * @return array + */ + public function execute( array $input ): array { + try { + $global_styles_id = isset( $input['id'] ) ? absint( $input['id'] ) : 0; + + if ( empty( $global_styles_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Global styles ID is required', 'airo-wp' ), + ); + } + + $global_styles_post = get_post( $global_styles_id ); + + if ( ! $global_styles_post || 'wp_global_styles' !== $global_styles_post->post_type ) { + return array( + 'success' => false, + 'message' => __( 'Global styles post not found or invalid ID', 'airo-wp' ), + ); + } + + $existing_content = array(); + if ( ! empty( $global_styles_post->post_content ) ) { + $decoded = json_decode( $global_styles_post->post_content, true ); + if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { + $existing_content = $decoded; + } else { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( + sprintf( + 'Global styles post %d has corrupted JSON content. Starting fresh. Error: %s', + $global_styles_id, + json_last_error_msg() + ) + ); + } + } + + $new_content = $existing_content; + $overwrite = isset( $input['overwrite'] ) && true === $input['overwrite']; + + if ( isset( $input['styles'] ) && is_array( $input['styles'] ) ) { + if ( $overwrite ) { + $new_content['styles'] = $input['styles']; + } else { + if ( ! isset( $new_content['styles'] ) ) { + $new_content['styles'] = array(); + } + $new_content['styles'] = $this->deep_merge_arrays( $new_content['styles'], $input['styles'] ); + } + } + + if ( isset( $input['settings'] ) && is_array( $input['settings'] ) ) { + if ( $overwrite ) { + $new_content['settings'] = $input['settings']; + } else { + if ( ! isset( $new_content['settings'] ) ) { + $new_content['settings'] = array(); + } + $new_content['settings'] = $this->deep_merge_arrays( $new_content['settings'], $input['settings'] ); + } + + if ( isset( $new_content['settings']['typography']['fontFamilies'] ) ) { + $new_content['settings']['typography']['fontFamilies'] = $this->font_downloader->process_font_families( + $new_content['settings']['typography']['fontFamilies'] + ); + } + } + + $encoded_content = wp_json_encode( $new_content ); + if ( false === $encoded_content ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: JSON encoding error message. */ + __( 'Failed to encode global styles: %s', 'airo-wp' ), + json_last_error_msg() + ), + ); + } + + $post_data = array( + 'ID' => $global_styles_id, + 'post_content' => $encoded_content, + ); + + if ( isset( $input['title'] ) && ! empty( $input['title'] ) ) { + $post_data['post_title'] = sanitize_text_field( $input['title'] ); + } + + $post_data = wp_slash( $post_data ); + + $update_result = wp_update_post( $post_data, true ); + + if ( is_wp_error( $update_result ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Failed to update global styles: %s', 'airo-wp' ), + $update_result->get_error_message() + ), + ); + } + + if ( ! $update_result ) { + return array( + 'success' => false, + 'message' => __( 'Failed to update global styles', 'airo-wp' ), + ); + } + + return array( + 'success' => true, + 'data' => array( + 'id' => (int) $update_result, + 'title' => isset( $input['title'] ) && ! empty( $input['title'] ) ? sanitize_text_field( $input['title'] ) : $global_styles_post->post_title, + 'content' => $new_content, + 'status' => $global_styles_post->post_status, + 'date' => current_time( 'mysql' ), + ), + 'message' => __( 'Global styles updated successfully', 'airo-wp' ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error updating global styles: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Deep merge two arrays recursively with intelligent array handling. + * + * @param array $existing The existing array. + * @param array $new_data The new array to merge. + * @return array + */ + private function deep_merge_arrays( array $existing, array $new_data ): array { + foreach ( $new_data as $key => $value ) { + if ( is_array( $value ) && isset( $existing[ $key ] ) && is_array( $existing[ $key ] ) ) { + if ( $this->is_list_of_objects( $value ) || $this->is_list_of_objects( $existing[ $key ] ) ) { + $existing[ $key ] = $value; + } else { + $existing[ $key ] = $this->deep_merge_arrays( $existing[ $key ], $value ); + } + } else { + $existing[ $key ] = $value; + } + } + return $existing; + } + + /** + * Check if an array is a numeric-indexed list of objects/arrays. + * + * @param array $value The array to check. + * @return bool True if this is a list of objects that should be replaced. + */ + private function is_list_of_objects( array $value ): bool { + if ( empty( $value ) ) { + return false; + } + + $keys = array_keys( $value ); + if ( range( 0, count( $value ) - 1 ) !== $keys ) { + return false; + } + + return is_array( $value[0] ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the global styles template to update', 'airo-wp' ), + ), + 'styles' => array( + 'type' => 'object', + 'description' => __( 'Global styles configuration object', 'airo-wp' ), + ), + 'settings' => array( + 'type' => 'object', + 'description' => __( 'Global settings configuration object', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'Title of the global styles variation', 'airo-wp' ), + ), + 'overwrite' => array( + 'type' => 'boolean', + 'description' => __( 'If true, replace entire styles/settings instead of merging. Defaults to false.', 'airo-wp' ), + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Updated global styles data', 'airo-wp' ), + array( + 'data' => array( + 'type' => 'object', + 'description' => __( 'Updated global styles data', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The global styles post ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The global styles title', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'object', + 'description' => __( 'The updated global styles configuration', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The post status', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The last modified date', 'airo-wp' ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Media/DeleteMedia.php b/includes/Mcp/Tools/Media/DeleteMedia.php new file mode 100644 index 0000000..a56bd91 --- /dev/null +++ b/includes/Mcp/Tools/Media/DeleteMedia.php @@ -0,0 +1,161 @@ + __( 'Delete Media', 'airo-wp' ), + 'description' => __( 'Deletes a WordPress media attachment', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'media-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Delete result or error. + */ + public function execute( array $input ): array { + $media_id = ! empty( $input['media_id'] ) ? (int) $input['media_id'] : 0; + + if ( empty( $media_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Media ID is required', 'airo-wp' ), + ); + } + + $post = get_post( $media_id ); + + if ( ! $post ) { + return array( + 'success' => false, + /* translators: %d: Media ID */ + 'message' => sprintf( __( 'Media with ID %d not found', 'airo-wp' ), $media_id ), + ); + } + + if ( 'attachment' !== $post->post_type ) { + return array( + 'success' => false, + /* translators: %d: Post ID */ + 'message' => sprintf( __( 'Post with ID %d is not a media attachment', 'airo-wp' ), $media_id ), + ); + } + + $media_info = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'url' => wp_get_attachment_url( $media_id ), + ); + + $deleted = wp_delete_attachment( $media_id, true ); + + if ( false === $deleted || null === $deleted ) { + return array( + 'success' => false, + 'message' => __( 'Failed to delete media attachment', 'airo-wp' ), + ); + } + + return array( + 'success' => true, + /* translators: %d: Media ID */ + 'message' => sprintf( __( 'Successfully deleted media with ID %d', 'airo-wp' ), $media_id ), + 'deleted_media' => $media_info, + ); + } + + /** + * Input JSON Schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'media_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the media attachment to delete', 'airo-wp' ), + 'minimum' => 1, + ), + ), + 'required' => array( 'media_id' ), + ); + } + + /** + * Output JSON Schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Media deletion result', 'airo-wp' ), + array( + 'deleted_media' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The deleted media attachment ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The title of the deleted media', 'airo-wp' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'The URL of the deleted media', 'airo-wp' ), + ), + ), + 'description' => __( 'Information about the deleted media (only present on success)', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Media/GetAllMedia.php b/includes/Mcp/Tools/Media/GetAllMedia.php new file mode 100644 index 0000000..4f40dbb --- /dev/null +++ b/includes/Mcp/Tools/Media/GetAllMedia.php @@ -0,0 +1,298 @@ + __( 'Get All Media', 'airo-wp' ), + 'description' => __( 'Retrieves all WordPress media attachments', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'media-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Media list. + */ + public function execute( array $input ): array { + $include_meta = isset( $input['include_meta'] ) ? (bool) $input['include_meta'] : false; + + $query = new \WP_Query( + array( + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'posts_per_page' => -1, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + if ( ! $query->have_posts() ) { + return array( + 'media' => array(), + 'total' => 0, + ); + } + + $media_items = array(); + + while ( $query->have_posts() ) { + $query->the_post(); + $post = get_post(); + + $attachment_metadata = wp_get_attachment_metadata( $post->ID ); + $file_path = get_attached_file( $post->ID ); + $file_size = $file_path && file_exists( $file_path ) ? filesize( $file_path ) : 0; + + $media_item = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'description' => $post->post_content, + 'caption' => $post->post_excerpt, + 'alt_text' => get_post_meta( $post->ID, '_wp_attachment_image_alt', true ), + 'status' => $post->post_status, + 'author_id' => (int) $post->post_author, + 'date_created' => $post->post_date, + 'date_modified' => $post->post_modified, + 'slug' => $post->post_name, + 'url' => wp_get_attachment_url( $post->ID ), + 'mime_type' => $post->post_mime_type, + 'file_size' => $file_size, + 'dimensions' => array(), + ); + + if ( isset( $attachment_metadata['width'] ) && isset( $attachment_metadata['height'] ) ) { + $media_item['dimensions'] = array( + 'width' => (int) $attachment_metadata['width'], + 'height' => (int) $attachment_metadata['height'], + ); + } + + if ( $include_meta ) { + $media_item['meta'] = $this->get_processed_meta( $post->ID, $attachment_metadata ); + } + + $media_items[] = $media_item; + } + + wp_reset_postdata(); + + return array( + 'media' => $media_items, + 'total' => count( $media_items ), + ); + } + + /** + * Input JSON Schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'include_meta' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include attachment meta data', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array(), + ); + } + + /** + * Output JSON Schema. + * + * @return array + */ + private function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'media' => array( + 'type' => 'array', + 'description' => __( 'Array of all media attachments', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The media attachment ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The media title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The media description', 'airo-wp' ), + ), + 'caption' => array( + 'type' => 'string', + 'description' => __( 'The media caption', 'airo-wp' ), + ), + 'alt_text' => array( + 'type' => 'string', + 'description' => __( 'The media alt text', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The attachment status', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The media author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The media creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The media modification date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The media slug', 'airo-wp' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'The media file URL', 'airo-wp' ), + ), + 'mime_type' => array( + 'type' => 'string', + 'description' => __( 'The media MIME type', 'airo-wp' ), + ), + 'file_size' => array( + 'type' => 'integer', + 'description' => __( 'The file size in bytes', 'airo-wp' ), + ), + 'dimensions' => array( + 'type' => 'object', + 'properties' => array( + 'width' => array( + 'type' => 'integer', + 'description' => __( 'Image width in pixels', 'airo-wp' ), + ), + 'height' => array( + 'type' => 'integer', + 'description' => __( 'Image height in pixels', 'airo-wp' ), + ), + ), + 'description' => __( 'Image dimensions (for images)', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'object', + 'description' => __( 'Attachment meta data (if requested)', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of media attachments', 'airo-wp' ), + ), + ), + ); + } + + /** + * Get processed meta data for an attachment. + * + * @param int $media_id The media attachment ID. + * @param array|bool $attachment_metadata The attachment metadata. + * @return array Processed meta data. + */ + private function get_processed_meta( int $media_id, $attachment_metadata ): array { + $meta = array(); + + if ( ! is_array( $attachment_metadata ) ) { + return $meta; + } + + $attached_file = get_post_meta( $media_id, '_wp_attached_file', true ); + if ( $attached_file ) { + $meta['attached_file'] = $attached_file; + } + + if ( isset( $attachment_metadata['sizes'] ) && is_array( $attachment_metadata['sizes'] ) ) { + $meta['image_sizes'] = array(); + foreach ( $attachment_metadata['sizes'] as $size_name => $size_data ) { + $meta['image_sizes'][ $size_name ] = array( + 'file' => $size_data['file'], + 'width' => $size_data['width'], + 'height' => $size_data['height'], + 'mime_type' => $size_data['mime-type'], + 'filesize' => isset( $size_data['filesize'] ) ? $size_data['filesize'] : 0, + ); + } + } + + if ( isset( $attachment_metadata['image_meta'] ) && is_array( $attachment_metadata['image_meta'] ) ) { + $image_meta = $attachment_metadata['image_meta']; + + $meta['image_meta'] = array( + 'aperture' => $image_meta['aperture'] ?? '', + 'credit' => $image_meta['credit'] ?? '', + 'camera' => $image_meta['camera'] ?? '', + 'caption' => $image_meta['caption'] ?? '', + 'created_timestamp' => $image_meta['created_timestamp'] ?? '', + 'copyright' => $image_meta['copyright'] ?? '', + 'focal_length' => $image_meta['focal_length'] ?? '', + 'iso' => $image_meta['iso'] ?? '', + 'shutter_speed' => $image_meta['shutter_speed'] ?? '', + 'title' => $image_meta['title'] ?? '', + 'orientation' => $image_meta['orientation'] ?? '', + 'keywords' => $image_meta['keywords'] ?? array(), + ); + } + + return $meta; + } +} diff --git a/includes/Mcp/Tools/Media/GetMediaById.php b/includes/Mcp/Tools/Media/GetMediaById.php new file mode 100644 index 0000000..47c8c26 --- /dev/null +++ b/includes/Mcp/Tools/Media/GetMediaById.php @@ -0,0 +1,231 @@ + __( 'Get Media By ID', 'airo-wp' ), + 'description' => __( 'Retrieves a WordPress media attachment by its ID', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'media-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Media information or error. + */ + public function execute( array $input ): array { + $media_id = ! empty( $input['media_id'] ) ? (int) $input['media_id'] : 0; + + if ( empty( $media_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Media ID is required', 'airo-wp' ), + ); + } + + $post = get_post( $media_id ); + + if ( ! $post ) { + return array( + 'success' => false, + /* translators: %d: Media ID */ + 'message' => sprintf( __( 'Media with ID %d not found', 'airo-wp' ), $media_id ), + ); + } + + if ( 'attachment' !== $post->post_type ) { + return array( + 'success' => false, + /* translators: %d: Post ID */ + 'message' => sprintf( __( 'Post with ID %d is not a media attachment', 'airo-wp' ), $media_id ), + ); + } + + $attachment_metadata = wp_get_attachment_metadata( $media_id ); + $file_path = get_attached_file( $media_id ); + $file_size = $file_path && file_exists( $file_path ) ? filesize( $file_path ) : 0; + + $result = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'description' => $post->post_content, + 'caption' => $post->post_excerpt, + 'alt_text' => get_post_meta( $media_id, '_wp_attachment_image_alt', true ), + 'status' => $post->post_status, + 'author_id' => (int) $post->post_author, + 'date_created' => $post->post_date, + 'date_modified' => $post->post_modified, + 'slug' => $post->post_name, + 'url' => wp_get_attachment_url( $media_id ), + 'mime_type' => $post->post_mime_type, + 'file_size' => $file_size, + 'dimensions' => array(), + 'meta' => array(), + ); + + if ( isset( $attachment_metadata['width'] ) && isset( $attachment_metadata['height'] ) ) { + $result['dimensions'] = array( + 'width' => (int) $attachment_metadata['width'], + 'height' => (int) $attachment_metadata['height'], + ); + } + + if ( ! empty( $input['include_meta'] ) ) { + $result['meta'] = get_post_meta( $media_id ); + } + + return $result; + } + + /** + * Input JSON Schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'media_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the media attachment to retrieve', 'airo-wp' ), + 'minimum' => 1, + ), + 'include_meta' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include attachment meta data', 'airo-wp' ), + 'default' => true, + ), + ), + 'required' => array( 'media_id' ), + ); + } + + /** + * Output JSON Schema. + * + * @return array + */ + private function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The media attachment ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The media title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The media description', 'airo-wp' ), + ), + 'caption' => array( + 'type' => 'string', + 'description' => __( 'The media caption', 'airo-wp' ), + ), + 'alt_text' => array( + 'type' => 'string', + 'description' => __( 'The media alt text', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The attachment status', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The media author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The media creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The media modification date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The media slug', 'airo-wp' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'The media file URL', 'airo-wp' ), + ), + 'mime_type' => array( + 'type' => 'string', + 'description' => __( 'The media MIME type', 'airo-wp' ), + ), + 'file_size' => array( + 'type' => 'integer', + 'description' => __( 'The file size in bytes', 'airo-wp' ), + ), + 'dimensions' => array( + 'type' => 'object', + 'properties' => array( + 'width' => array( + 'type' => 'integer', + 'description' => __( 'Image width in pixels', 'airo-wp' ), + ), + 'height' => array( + 'type' => 'integer', + 'description' => __( 'Image height in pixels', 'airo-wp' ), + ), + ), + 'description' => __( 'Image dimensions (for images)', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'object', + 'description' => __( 'Attachment meta data (if requested)', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Media/ListMedia.php b/includes/Mcp/Tools/Media/ListMedia.php new file mode 100644 index 0000000..9fb2fa6 --- /dev/null +++ b/includes/Mcp/Tools/Media/ListMedia.php @@ -0,0 +1,578 @@ + __( 'List Media', 'airo-wp' ), + 'description' => __( 'Retrieves a list of WordPress media attachments with filtering and pagination options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'media-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array List of media with pagination info. + */ + public function execute( array $input ): array { + $query_args = $this->build_query_args( $input ); + + // WP_Query's `s` only searches post_title/excerpt/content. For attachments, + // post_title is the original filename (with spaces) while users typically + // know the sanitized filename (with dashes), so title-only search misses. + // _filter_query_attachment_filenames extends the LIKE clause to the + // _wp_attached_file postmeta — same approach core REST /wp/v2/media uses. + // The filter self-removes on first invocation. + $has_search = ! empty( $input['search'] ); + if ( $has_search ) { + add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + } + + $query = new \WP_Query( $query_args ); + + $media = array(); + $include_meta = ! empty( $input['include_meta'] ); + $fields_filter = ! empty( $input['_fields'] ) && is_array( $input['_fields'] ) ? $input['_fields'] : array(); + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + if ( $query->have_posts() ) { + while ( $query->have_posts() ) { + $query->the_post(); + $post = get_post(); + + $complete_media_data = $this->build_media_data( $post, $context, $include_meta ); + + if ( ! empty( $fields_filter ) ) { + $media_data = $this->filter_fields( $complete_media_data, $fields_filter ); + } else { + $media_data = $complete_media_data; + } + + $media[] = $media_data; + } + wp_reset_postdata(); + } + + $page = isset( $input['page'] ) ? (int) $input['page'] : 1; + $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 10; + $total = $query->found_posts; + $total_pages = max( 1, (int) ceil( $total / $per_page ) ); + + return array( + 'media' => $media, + 'total' => $total, + 'total_pages' => $total_pages, + 'page' => $page, + 'per_page' => $per_page, + ); + } + + /** + * Build WP_Query arguments from input parameters. + * + * @param array $input Input parameters. + * @return array WP_Query arguments. + */ + private function build_query_args( array $input ): array { + $args = array( + 'post_type' => 'attachment', + 'post_status' => isset( $input['status'] ) ? $input['status'] : 'inherit', + 'posts_per_page' => isset( $input['per_page'] ) ? (int) $input['per_page'] : 10, + 'paged' => isset( $input['page'] ) ? (int) $input['page'] : 1, + 'order' => isset( $input['order'] ) ? strtoupper( $input['order'] ) : 'DESC', + 'orderby' => isset( $input['orderby'] ) ? $input['orderby'] : 'date', + ); + + if ( ! empty( $input['search'] ) ) { + $args['s'] = sanitize_text_field( $input['search'] ); + } + + if ( isset( $input['author'] ) ) { + $args['author'] = (int) $input['author']; + } + + if ( ! empty( $input['author_exclude'] ) && is_array( $input['author_exclude'] ) ) { + $args['author__not_in'] = array_map( 'intval', $input['author_exclude'] ); + } + + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) { + $args['post__not_in'] = array_map( 'intval', $input['exclude'] ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- bounded input from MCP schema. + } + + if ( ! empty( $input['include'] ) && is_array( $input['include'] ) ) { + $args['post__in'] = array_map( 'intval', $input['include'] ); + if ( 'include' === $args['orderby'] ) { + $args['orderby'] = 'post__in'; + } + } + + if ( isset( $input['offset'] ) ) { + $args['offset'] = (int) $input['offset']; + } + + if ( isset( $input['parent'] ) ) { + $args['post_parent'] = (int) $input['parent']; + } + + if ( ! empty( $input['parent_exclude'] ) && is_array( $input['parent_exclude'] ) ) { + $args['post_parent__not_in'] = array_map( 'intval', $input['parent_exclude'] ); + } + + if ( ! empty( $input['slug'] ) ) { + if ( is_array( $input['slug'] ) ) { + $args['post_name__in'] = array_map( 'sanitize_title', $input['slug'] ); + } else { + $args['post_name__in'] = array( sanitize_title( $input['slug'] ) ); + } + } + + if ( ! empty( $input['media_type'] ) ) { + $args['post_mime_type'] = sanitize_text_field( $input['media_type'] ); + } + + if ( ! empty( $input['mime_type'] ) ) { + $args['post_mime_type'] = sanitize_mime_type( $input['mime_type'] ); + } + + $date_query = array(); + + if ( ! empty( $input['after'] ) ) { + $date_query[] = array( + 'after' => $input['after'], + 'inclusive' => true, + 'column' => 'post_date', + ); + } + + if ( ! empty( $input['before'] ) ) { + $date_query[] = array( + 'before' => $input['before'], + 'inclusive' => true, + 'column' => 'post_date', + ); + } + + if ( ! empty( $input['modified_after'] ) ) { + $date_query[] = array( + 'after' => $input['modified_after'], + 'inclusive' => true, + 'column' => 'post_modified', + ); + } + + if ( ! empty( $input['modified_before'] ) ) { + $date_query[] = array( + 'before' => $input['modified_before'], + 'inclusive' => true, + 'column' => 'post_modified', + ); + } + + if ( ! empty( $date_query ) ) { + $args['date_query'] = $date_query; + } + + return $args; + } + + /** + * Build media data for a single post. + * + * @param \WP_Post $post Post object. + * @param string $context Context (view, embed, edit). + * @param bool $include_meta Whether to include meta data. + * @return array Media data. + */ + private function build_media_data( \WP_Post $post, string $context, bool $include_meta ): array { + $attachment_metadata = wp_get_attachment_metadata( $post->ID ); + $file_path = get_attached_file( $post->ID ); + $file_size = $file_path && file_exists( $file_path ) ? filesize( $file_path ) : 0; + + $media_data = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'description' => $post->post_content, + 'caption' => $post->post_excerpt, + 'alt_text' => get_post_meta( $post->ID, '_wp_attachment_image_alt', true ), + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'author_id' => (int) $post->post_author, + 'date_created' => $post->post_date, + 'date_modified' => $post->post_modified, + 'url' => wp_get_attachment_url( $post->ID ), + 'mime_type' => $post->post_mime_type, + 'file_size' => $file_size, + 'dimensions' => array(), + ); + + if ( isset( $attachment_metadata['width'] ) && isset( $attachment_metadata['height'] ) ) { + $media_data['dimensions'] = array( + 'width' => (int) $attachment_metadata['width'], + 'height' => (int) $attachment_metadata['height'], + ); + } + + if ( $include_meta ) { + $media_data['meta'] = $this->get_processed_meta( $post->ID, $attachment_metadata ); + } + + return $media_data; + } + + /** + * Get processed meta data for an attachment. + * + * @param int $media_id The media attachment ID. + * @param array|bool $attachment_metadata The attachment metadata. + * @return array Processed meta data. + */ + private function get_processed_meta( int $media_id, $attachment_metadata ): array { + $meta = array(); + + if ( ! is_array( $attachment_metadata ) ) { + return $meta; + } + + $attached_file = get_post_meta( $media_id, '_wp_attached_file', true ); + if ( $attached_file ) { + $meta['attached_file'] = $attached_file; + } + + if ( isset( $attachment_metadata['sizes'] ) && is_array( $attachment_metadata['sizes'] ) ) { + $meta['image_sizes'] = array(); + foreach ( $attachment_metadata['sizes'] as $size_name => $size_data ) { + $meta['image_sizes'][ $size_name ] = array( + 'file' => $size_data['file'], + 'width' => $size_data['width'], + 'height' => $size_data['height'], + 'mime_type' => $size_data['mime-type'], + 'filesize' => isset( $size_data['filesize'] ) ? $size_data['filesize'] : 0, + ); + } + } + + if ( isset( $attachment_metadata['image_meta'] ) && is_array( $attachment_metadata['image_meta'] ) ) { + $image_meta = $attachment_metadata['image_meta']; + + $meta['image_meta'] = array( + 'aperture' => $image_meta['aperture'] ?? '', + 'credit' => $image_meta['credit'] ?? '', + 'camera' => $image_meta['camera'] ?? '', + 'caption' => $image_meta['caption'] ?? '', + 'created_timestamp' => $image_meta['created_timestamp'] ?? '', + 'copyright' => $image_meta['copyright'] ?? '', + 'focal_length' => $image_meta['focal_length'] ?? '', + 'iso' => $image_meta['iso'] ?? '', + 'shutter_speed' => $image_meta['shutter_speed'] ?? '', + 'title' => $image_meta['title'] ?? '', + 'orientation' => $image_meta['orientation'] ?? '', + 'keywords' => $image_meta['keywords'] ?? array(), + ); + } + + return $meta; + } + + /** + * Filter media data to only include specified fields. + * + * @param array $media_data Complete media data. + * @param array $fields Array of field names to include. + * @return array Filtered media data. + */ + private function filter_fields( array $media_data, array $fields ): array { + $filtered = array(); + + if ( isset( $media_data['id'] ) ) { + $filtered['id'] = $media_data['id']; + } + + foreach ( $fields as $field ) { + $field = sanitize_key( $field ); + if ( isset( $media_data[ $field ] ) && 'id' !== $field ) { + $filtered[ $field ] = $media_data[ $field ]; + } + } + + return $filtered; + } + + /** + * Input JSON Schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to be returned in result set', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Limit results to those matching a string', 'airo-wp' ), + ), + 'after' => array( + 'type' => 'string', + 'description' => __( 'Limit response to media uploaded after a given ISO8601 compliant date', 'airo-wp' ), + ), + 'modified_after' => array( + 'type' => 'string', + 'description' => __( 'Limit response to media modified after a given ISO8601 compliant date', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Limit result set to media assigned to specific author ID', 'airo-wp' ), + ), + 'author_exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes media assigned to specific author IDs', 'airo-wp' ), + ), + 'before' => array( + 'type' => 'string', + 'description' => __( 'Limit response to media uploaded before a given ISO8601 compliant date', 'airo-wp' ), + ), + 'modified_before' => array( + 'type' => 'string', + 'description' => __( 'Limit response to media modified before a given ISO8601 compliant date', 'airo-wp' ), + ), + 'exclude' => array( // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- mirrors WP REST API exclude param; bounded by schema. + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes specific IDs', 'airo-wp' ), + ), + 'include' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to specific IDs', 'airo-wp' ), + ), + 'offset' => array( + 'type' => 'integer', + 'description' => __( 'Offset the result set by a specific number of items', 'airo-wp' ), + 'minimum' => 0, + ), + 'order' => array( + 'type' => 'string', + 'description' => __( 'Order sort attribute ascending or descending', 'airo-wp' ), + 'enum' => array( 'asc', 'desc' ), + 'default' => 'desc', + ), + 'orderby' => array( + 'type' => 'string', + 'description' => __( 'Sort collection by media attribute', 'airo-wp' ), + 'enum' => array( 'author', 'date', 'id', 'include', 'modified', 'parent', 'relevance', 'slug', 'title' ), + 'default' => 'date', + ), + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'Limit result set to media attached to a particular parent ID', 'airo-wp' ), + ), + 'parent_exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to all media except those of a particular parent ID', 'airo-wp' ), + ), + 'slug' => array( + 'description' => __( 'Limit result set to media with one or more specific slugs', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Limit result set to media assigned one or more statuses', 'airo-wp' ), + 'enum' => array( 'inherit', 'private', 'trash' ), + 'default' => 'inherit', + ), + 'media_type' => array( + 'type' => 'string', + 'description' => __( 'Limit result set to media of a particular media type', 'airo-wp' ), + 'enum' => array( 'image', 'video', 'audio', 'application' ), + ), + 'mime_type' => array( + 'type' => 'string', + 'description' => __( 'Limit result set to media of a particular MIME type', 'airo-wp' ), + ), + 'include_meta' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include attachment meta data', 'airo-wp' ), + 'default' => false, + ), + '_fields' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + 'description' => __( 'Limit response to specific fields', 'airo-wp' ), + ), + ), + ); + } + + /** + * Output JSON Schema. + * + * @return array + */ + private function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'media' => array( + 'type' => 'array', + 'description' => __( 'Array of media objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The media attachment ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The media title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The media description', 'airo-wp' ), + ), + 'caption' => array( + 'type' => 'string', + 'description' => __( 'The media caption', 'airo-wp' ), + ), + 'alt_text' => array( + 'type' => 'string', + 'description' => __( 'The media alt text', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The attachment status', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The media author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The media creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The media modification date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The media slug', 'airo-wp' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'The media file URL', 'airo-wp' ), + ), + 'mime_type' => array( + 'type' => 'string', + 'description' => __( 'The media MIME type', 'airo-wp' ), + ), + 'file_size' => array( + 'type' => 'integer', + 'description' => __( 'The file size in bytes', 'airo-wp' ), + ), + 'dimensions' => array( + 'type' => 'object', + 'properties' => array( + 'width' => array( + 'type' => 'integer', + 'description' => __( 'Image width in pixels', 'airo-wp' ), + ), + 'height' => array( + 'type' => 'integer', + 'description' => __( 'Image height in pixels', 'airo-wp' ), + ), + ), + 'description' => __( 'Image dimensions (for images)', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'object', + 'description' => __( 'Attachment meta data (if requested)', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of media items matching the query', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages available', 'airo-wp' ), + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page number', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of items per page', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Media/UpdateMediaMeta.php b/includes/Mcp/Tools/Media/UpdateMediaMeta.php new file mode 100644 index 0000000..2af853b --- /dev/null +++ b/includes/Mcp/Tools/Media/UpdateMediaMeta.php @@ -0,0 +1,308 @@ + __( 'Update Media Meta', 'airo-wp' ), + 'description' => __( 'Updates the title, description, alt text, and caption of a WordPress media attachment', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'media-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Update result or error. + */ + public function execute( array $input ): array { + $media_id = ! empty( $input['media_id'] ) ? (int) $input['media_id'] : 0; + + if ( empty( $media_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Media ID is required', 'airo-wp' ), + ); + } + + $updatable_fields = array( 'title', 'description', 'alt_text', 'caption' ); + $has_update_field = false; + + foreach ( $updatable_fields as $field ) { + if ( isset( $input[ $field ] ) ) { + $has_update_field = true; + break; + } + } + + if ( ! $has_update_field ) { + return array( + 'success' => false, + 'message' => __( 'At least one field (title, description, alt_text, or caption) is required to update', 'airo-wp' ), + ); + } + + $post = get_post( $media_id ); + + if ( ! $post ) { + return array( + 'success' => false, + /* translators: %d: Media ID */ + 'message' => sprintf( __( 'Media with ID %d not found', 'airo-wp' ), $media_id ), + ); + } + + if ( 'attachment' !== $post->post_type ) { + return array( + 'success' => false, + /* translators: %d: Post ID */ + 'message' => sprintf( __( 'Post with ID %d is not a media attachment', 'airo-wp' ), $media_id ), + ); + } + + $update_data = array( 'ID' => $media_id ); + $updated_fields = array(); + + if ( isset( $input['title'] ) ) { + $update_data['post_title'] = sanitize_text_field( $input['title'] ); + $updated_fields[] = 'title'; + } + + if ( isset( $input['description'] ) ) { + $update_data['post_content'] = wp_kses_post( $input['description'] ); + $updated_fields[] = 'description'; + } + + if ( isset( $input['caption'] ) ) { + $update_data['post_excerpt'] = wp_kses_post( $input['caption'] ); + $updated_fields[] = 'caption'; + } + + if ( count( $update_data ) > 1 ) { + $result = wp_update_post( $update_data, true ); + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + /* translators: %s: Error message */ + 'message' => sprintf( __( 'Failed to update media: %s', 'airo-wp' ), $result->get_error_message() ), + ); + } + } + + if ( isset( $input['alt_text'] ) ) { + $alt_text_result = update_post_meta( $media_id, '_wp_attachment_image_alt', sanitize_text_field( $input['alt_text'] ) ); + + if ( false === $alt_text_result ) { + return array( + 'success' => false, + 'message' => __( 'Failed to update alt text', 'airo-wp' ), + ); + } + + $updated_fields[] = 'alt_text'; + } + + $updated_post = get_post( $media_id ); + + $attachment_metadata = wp_get_attachment_metadata( $media_id ); + $file_path = get_attached_file( $media_id ); + $file_size = $file_path && file_exists( $file_path ) ? filesize( $file_path ) : 0; + + $dimensions = array(); + if ( ! empty( $attachment_metadata['width'] ) && ! empty( $attachment_metadata['height'] ) ) { + $dimensions = array( + 'width' => (int) $attachment_metadata['width'], + 'height' => (int) $attachment_metadata['height'], + ); + } + + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %1$s: comma-separated list of updated fields, %2$d: Media ID */ + __( 'Successfully updated %1$s for media ID %2$d', 'airo-wp' ), + implode( ', ', $updated_fields ), + $media_id + ), + 'updated_media' => array( + 'id' => $updated_post->ID, + 'title' => $updated_post->post_title, + 'description' => $updated_post->post_content, + 'alt_text' => get_post_meta( $media_id, '_wp_attachment_image_alt', true ), + 'caption' => $updated_post->post_excerpt, + 'status' => $updated_post->post_status, + 'author_id' => (int) $updated_post->post_author, + 'date_created' => $updated_post->post_date, + 'date_modified' => $updated_post->post_modified, + 'slug' => $updated_post->post_name, + 'url' => wp_get_attachment_url( $media_id ), + 'mime_type' => $updated_post->post_mime_type, + 'file_size' => $file_size, + 'dimensions' => $dimensions, + ), + ); + } + + /** + * Input JSON Schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'media_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the media attachment to update', 'airo-wp' ), + 'minimum' => 1, + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The new title for the media attachment', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The new description for the media attachment', 'airo-wp' ), + ), + 'alt_text' => array( + 'type' => 'string', + 'description' => __( 'The new alt text for the media attachment', 'airo-wp' ), + ), + 'caption' => array( + 'type' => 'string', + 'description' => __( 'The new caption for the media attachment', 'airo-wp' ), + ), + ), + 'required' => array( 'media_id' ), + ); + } + + /** + * Output JSON Schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Media meta update result', 'airo-wp' ), + array( + 'updated_media' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The media attachment ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The updated media title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The updated media description', 'airo-wp' ), + ), + 'alt_text' => array( + 'type' => 'string', + 'description' => __( 'The updated media alt text', 'airo-wp' ), + ), + 'caption' => array( + 'type' => 'string', + 'description' => __( 'The updated media caption', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The attachment status', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The media author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The date the media was created', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The date the media was last modified', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The media slug', 'airo-wp' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'The URL of the media file', 'airo-wp' ), + ), + 'mime_type' => array( + 'type' => 'string', + 'description' => __( 'The MIME type of the media file', 'airo-wp' ), + ), + 'file_size' => array( + 'type' => 'integer', + 'description' => __( 'The file size in bytes', 'airo-wp' ), + ), + 'dimensions' => array( + 'type' => 'object', + 'description' => __( 'The image dimensions (width and height)', 'airo-wp' ), + 'properties' => array( + 'width' => array( + 'type' => 'integer', + 'description' => __( 'The image width in pixels', 'airo-wp' ), + ), + 'height' => array( + 'type' => 'integer', + 'description' => __( 'The image height in pixels', 'airo-wp' ), + ), + ), + ), + ), + 'description' => __( 'The updated media information (only present on success)', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Media/UploadImage.php b/includes/Mcp/Tools/Media/UploadImage.php new file mode 100644 index 0000000..96b092f --- /dev/null +++ b/includes/Mcp/Tools/Media/UploadImage.php @@ -0,0 +1,224 @@ + __( 'Upload Image', 'airo-wp' ), + 'description' => __( 'Downloads an image from a URL and uploads it to the WordPress media library, optionally attaching it to a post', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'media-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Upload result or error. + */ + public function execute( array $input ): array { + if ( empty( $input['url'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Image URL is required', 'airo-wp' ), + ); + } + + // Sanitize and validate URL. + $url = esc_url_raw( $input['url'] ); + + if ( ! $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) { + return array( + 'success' => false, + 'message' => __( 'Invalid URL provided', 'airo-wp' ), + ); + } + + // Get post ID if provided, otherwise null. + $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : null; + + // Validate post ID only if one was provided. + if ( $post_id && ! get_post( $post_id ) ) { + return array( + 'success' => false, + /* translators: %d: Post ID */ + 'message' => sprintf( __( 'Post with ID %d does not exist', 'airo-wp' ), $post_id ), + ); + } + + // Get optional title. + $title = isset( $input['title'] ) ? sanitize_text_field( $input['title'] ) : null; + + // Stage metadata to apply after upload (avoid multiple DB writes). + $metadata_updates = array(); + + if ( isset( $input['alt_text'] ) ) { + $metadata_updates['alt_text'] = sanitize_text_field( $input['alt_text'] ); + } + + if ( isset( $input['description'] ) ) { + $metadata_updates['post_content'] = wp_kses_post( $input['description'] ); + } + + if ( isset( $input['caption'] ) ) { + $metadata_updates['caption'] = wp_kses_post( $input['caption'] ); + } + + // Include media handling functions. + if ( ! function_exists( 'media_sideload_image' ) ) { + $this->load_admin_file( 'media.php' ); + $this->load_admin_file( 'file.php' ); + $this->load_admin_file( 'image.php' ); + } + + // Upload the image and get the attachment ID. + $attachment_id = media_sideload_image( $url, $post_id, $title, 'id' ); + + if ( is_wp_error( $attachment_id ) ) { + return array( + 'success' => false, + /* translators: %s: Error message */ + 'message' => sprintf( __( 'Failed to upload image: %s', 'airo-wp' ), $attachment_id->get_error_message() ), + ); + } + + // Apply staged metadata updates in a single pass. + if ( ! empty( $metadata_updates ) ) { + $post_update = array( 'ID' => $attachment_id ); + + // Update caption (post_excerpt) if provided. + if ( isset( $metadata_updates['caption'] ) ) { + $post_update['post_excerpt'] = $metadata_updates['caption']; + } + + // Update description (post_content) if provided. + if ( isset( $metadata_updates['post_content'] ) ) { + $post_update['post_content'] = $metadata_updates['post_content']; + } + + // Update post data if we have changes. + if ( count( $post_update ) > 1 ) { + wp_update_post( $post_update, true ); + } + + // Update alt text (post meta) if provided. + if ( isset( $metadata_updates['alt_text'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', $metadata_updates['alt_text'] ); + } + } + + // Get the attachment URL. + $attachment_url = wp_get_attachment_url( $attachment_id ); + + return array( + 'success' => true, + 'attachment_id' => $attachment_id, + 'url' => $attachment_url, + 'message' => __( 'Image uploaded successfully', 'airo-wp' ), + ); + } + + /** + * Input JSON Schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'url' => array( + 'type' => 'string', + 'description' => __( 'The URL of the image to download and upload', 'airo-wp' ), + ), + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'Optional. The post ID to attach the image to', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'Optional. The title for the image', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'Optional. The description for the image', 'airo-wp' ), + ), + 'alt_text' => array( + 'type' => 'string', + 'description' => __( 'Optional. The alt text for the image', 'airo-wp' ), + ), + 'caption' => array( + 'type' => 'string', + 'description' => __( 'Optional. The caption for the image', 'airo-wp' ), + ), + ), + 'required' => array( 'url' ), + ); + } + + /** + * Output JSON Schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Image upload result', 'airo-wp' ), + array( + 'attachment_id' => array( + 'type' => 'integer', + 'description' => __( 'The attachment ID of the uploaded image', 'airo-wp' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'The URL of the uploaded image', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Navigation/CreateNavigation.php b/includes/Mcp/Tools/Navigation/CreateNavigation.php new file mode 100644 index 0000000..dbdb5d7 --- /dev/null +++ b/includes/Mcp/Tools/Navigation/CreateNavigation.php @@ -0,0 +1,207 @@ + __( 'Create Navigation', 'airo-wp' ), + 'description' => __( 'Creates a new navigation post', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array + */ + public function execute( array $input ): array { + $post_data = array( + 'post_type' => 'wp_navigation', + 'post_status' => isset( $input['status'] ) ? $input['status'] : 'publish', + ); + + if ( ! empty( $input['title'] ) ) { + $post_data['post_title'] = sanitize_text_field( $input['title'] ); + } + + if ( ! empty( $input['content'] ) ) { + $post_data['post_content'] = $input['content']; + } + + if ( ! empty( $input['slug'] ) ) { + $post_data['post_name'] = sanitize_title( $input['slug'] ); + } + + if ( ! empty( $input['password'] ) ) { + $post_data['post_password'] = $input['password']; + } + + if ( ! empty( $input['date'] ) ) { + $post_data['post_date'] = $input['date']; + } + + if ( ! empty( $input['date_gmt'] ) ) { + $post_data['post_date_gmt'] = $input['date_gmt']; + } + + $post_id = wp_insert_post( $post_data, true ); + + if ( is_wp_error( $post_id ) ) { + return array( + 'success' => false, + 'message' => $post_id->get_error_message(), + ); + } + + wp_save_post_revision( $post_id ); + + if ( ! empty( $input['template'] ) ) { + update_post_meta( $post_id, '_wp_page_template', sanitize_text_field( $input['template'] ) ); + } + + $post = get_post( $post_id ); + + return array( + 'success' => true, + 'navigation' => $this->build_navigation_data( $post ), + 'message' => __( 'Navigation created successfully', 'airo-wp' ), + ); + } + + /** + * Build navigation data for response. + * + * @param \WP_Post $post Post object. + * @return array Navigation data. + */ + private function build_navigation_data( \WP_Post $post ): array { + return array( + 'id' => $post->ID, + 'date' => $post->post_date, + 'date_gmt' => $post->post_date_gmt, + 'guid' => array( + 'rendered' => $post->guid, + 'raw' => $post->guid, + ), + 'modified' => $post->post_modified, + 'modified_gmt' => $post->post_modified_gmt, + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'type' => $post->post_type, + 'link' => get_permalink( $post->ID ), + 'title' => array( + 'rendered' => get_the_title( $post->ID ), + 'raw' => $post->post_title, + ), + 'content' => array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + 'raw' => $post->post_content, + ), + 'template' => get_page_template_slug( $post->ID ), + ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'date' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, in the site\'s timezone', 'airo-wp' ), + ), + 'date_gmt' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, as GMT', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'An alphanumeric identifier for the post unique to its type', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'A named status for the post', 'airo-wp' ), + 'enum' => array( 'publish', 'future', 'draft', 'pending', 'private' ), + 'default' => 'publish', + ), + 'password' => array( + 'type' => 'string', + 'description' => __( 'A password to protect access to the content and excerpt', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The title for the post', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The content for the post', 'airo-wp' ), + ), + 'template' => array( + 'type' => 'string', + 'description' => __( 'The theme file to use to display the post', 'airo-wp' ), + ), + ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Navigation creation result', 'airo-wp' ), + array( + 'navigation' => array( + 'type' => 'object', + 'description' => __( 'The created navigation data', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Navigation/DeleteNavigation.php b/includes/Mcp/Tools/Navigation/DeleteNavigation.php new file mode 100644 index 0000000..af281e5 --- /dev/null +++ b/includes/Mcp/Tools/Navigation/DeleteNavigation.php @@ -0,0 +1,171 @@ + __( 'Delete Navigation', 'airo-wp' ), + 'description' => __( 'Deletes a navigation post (trash or permanently)', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array + */ + public function execute( array $input ): array { + if ( empty( $input['id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Navigation ID is required', 'airo-wp' ), + ); + } + + $navigation_id = (int) $input['id']; + $force_delete = ! empty( $input['force'] ); + + $post = get_post( $navigation_id ); + if ( ! $post || 'wp_navigation' !== $post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: Navigation post ID */ + __( 'Navigation with ID %d not found', 'airo-wp' ), + $navigation_id + ), + ); + } + + if ( ! current_user_can( 'delete_post', $navigation_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to delete this navigation', 'airo-wp' ), + ); + } + + $previous = array( + 'id' => $post->ID, + 'date' => $post->post_date, + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'type' => $post->post_type, + 'title' => array( + 'rendered' => get_the_title( $post->ID ), + 'raw' => $post->post_title, + ), + 'content' => array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + 'raw' => $post->post_content, + ), + ); + + $result = wp_delete_post( $navigation_id, $force_delete ); + + if ( ! $result ) { + return array( + 'success' => false, + 'message' => __( 'Failed to delete navigation', 'airo-wp' ), + ); + } + + $message = $force_delete + ? __( 'Navigation permanently deleted', 'airo-wp' ) + : __( 'Navigation moved to trash', 'airo-wp' ); + + return array( + 'success' => true, + 'message' => $message, + 'deleted' => array( + 'id' => $navigation_id, + 'status' => $post->post_status, + ), + 'previous' => $previous, + ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the post', 'airo-wp' ), + 'minimum' => 1, + ), + 'force' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to bypass Trash and force deletion', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Navigation deletion result', 'airo-wp' ), + array( + 'deleted' => array( + 'type' => 'object', + 'description' => __( 'Information about the deleted navigation', 'airo-wp' ), + ), + 'previous' => array( + 'type' => 'object', + 'description' => __( 'The navigation data before deletion', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Navigation/GetNavigation.php b/includes/Mcp/Tools/Navigation/GetNavigation.php new file mode 100644 index 0000000..75f6cc4 --- /dev/null +++ b/includes/Mcp/Tools/Navigation/GetNavigation.php @@ -0,0 +1,253 @@ + __( 'Get Navigation', 'airo-wp' ), + 'description' => __( 'Retrieves a specific navigation post by ID', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array + */ + public function execute( array $input ): array { + if ( empty( $input['id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Navigation ID is required', 'airo-wp' ), + ); + } + + $navigation_id = (int) $input['id']; + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + $post = get_post( $navigation_id ); + + if ( ! $post || 'wp_navigation' !== $post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: Navigation post ID */ + __( 'Navigation with ID %d not found', 'airo-wp' ), + $navigation_id + ), + ); + } + + if ( ! empty( $post->post_password ) ) { + $password = isset( $input['password'] ) ? $input['password'] : ''; + if ( $password !== $post->post_password ) { + return array( + 'success' => false, + 'message' => __( 'Incorrect password for protected navigation', 'airo-wp' ), + ); + } + } + + if ( 'edit' === $context && ! current_user_can( 'edit_post', $navigation_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to edit this navigation', 'airo-wp' ), + ); + } + + return array( + 'success' => true, + 'navigation' => $this->build_navigation_data( $post, $context ), + ); + } + + /** + * Build navigation data based on context. + * + * @param \WP_Post $post Post object. + * @param string $context Response context. + * @return array Navigation data. + */ + private function build_navigation_data( \WP_Post $post, string $context ): array { + $data = array( + 'id' => $post->ID, + 'date' => $post->post_date, + 'date_gmt' => $post->post_date_gmt, + 'guid' => array( + 'rendered' => $post->guid, + 'raw' => $post->guid, + ), + 'modified' => $post->post_modified, + 'modified_gmt' => $post->post_modified_gmt, + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'type' => $post->post_type, + 'link' => get_permalink( $post->ID ), + 'title' => array( + 'rendered' => get_the_title( $post->ID ), + ), + ); + + if ( 'edit' === $context ) { + $data['title']['raw'] = $post->post_title; + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + 'raw' => $post->post_content, + ); + $data['template'] = get_page_template_slug( $post->ID ); + } elseif ( 'view' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + ); + $data['template'] = get_page_template_slug( $post->ID ); + } elseif ( 'embed' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + ); + } + + return $data; + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the post', 'airo-wp' ), + 'minimum' => 1, + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'password' => array( + 'type' => 'string', + 'description' => __( 'The password for the post if it is password protected', 'airo-wp' ), + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Navigation retrieval result', 'airo-wp' ), + array( + 'navigation' => array( + 'type' => 'object', + 'description' => __( 'The navigation data', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the post', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, in the site\'s timezone', 'airo-wp' ), + ), + 'date_gmt' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, as GMT', 'airo-wp' ), + ), + 'guid' => array( + 'type' => 'object', + 'description' => __( 'The globally unique identifier for the post', 'airo-wp' ), + ), + 'modified' => array( + 'type' => 'string', + 'description' => __( 'The date the post was last modified, in the site\'s timezone', 'airo-wp' ), + ), + 'modified_gmt' => array( + 'type' => 'string', + 'description' => __( 'The date the post was last modified, as GMT', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'An alphanumeric identifier for the post unique to its type', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'A named status for the post', 'airo-wp' ), + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Type of post', 'airo-wp' ), + ), + 'link' => array( + 'type' => 'string', + 'description' => __( 'URL to the post', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'object', + 'description' => __( 'The title for the post', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'object', + 'description' => __( 'The content for the post', 'airo-wp' ), + ), + 'template' => array( + 'type' => 'string', + 'description' => __( 'The theme file to use to display the post', 'airo-wp' ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Navigation/ListNavigationRevisions.php b/includes/Mcp/Tools/Navigation/ListNavigationRevisions.php new file mode 100644 index 0000000..7f52624 --- /dev/null +++ b/includes/Mcp/Tools/Navigation/ListNavigationRevisions.php @@ -0,0 +1,299 @@ + __( 'List Navigation Revisions', 'airo-wp' ), + 'description' => __( 'Retrieves a list of revisions for a specific navigation post with filtering and pagination options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array + */ + public function execute( array $input ): array { + if ( empty( $input['parent'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Parent navigation ID is required', 'airo-wp' ), + ); + } + + $parent_id = (int) $input['parent']; + + $parent_post = get_post( $parent_id ); + if ( ! $parent_post || 'wp_navigation' !== $parent_post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: Navigation post ID */ + __( 'Navigation with ID %d not found', 'airo-wp' ), + $parent_id + ), + ); + } + + if ( ! current_user_can( 'edit_post', $parent_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to view revisions for this navigation', 'airo-wp' ), + ); + } + + $args = $this->build_query_args( $input, $parent_id ); + + $revisions_query = new \WP_Query( $args ); + + $revisions = array(); + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + if ( $revisions_query->have_posts() ) { + while ( $revisions_query->have_posts() ) { + $revisions_query->the_post(); + $revision = get_post(); + + $revisions[] = $this->build_revision_data( $revision, $context ); + } + wp_reset_postdata(); + } + + return array( + 'success' => true, + 'revisions' => $revisions, + 'total' => (int) $revisions_query->found_posts, + 'total_pages' => (int) $revisions_query->max_num_pages, + 'page' => (int) ( isset( $input['page'] ) ? $input['page'] : 1 ), + 'per_page' => (int) ( isset( $input['per_page'] ) ? $input['per_page'] : 10 ), + ); + } + + /** + * Build WP_Query arguments from input parameters. + * + * @param array $input Input parameters. + * @param int $parent_id Parent navigation ID. + * @return array WP_Query arguments. + */ + private function build_query_args( array $input, int $parent_id ): array { + $args = array( + 'post_type' => 'revision', + 'post_parent' => $parent_id, + 'post_status' => 'inherit', + 'posts_per_page' => isset( $input['per_page'] ) ? (int) $input['per_page'] : 10, + 'paged' => isset( $input['page'] ) ? (int) $input['page'] : 1, + 'orderby' => isset( $input['orderby'] ) ? $input['orderby'] : 'date', + 'order' => isset( $input['order'] ) ? strtoupper( $input['order'] ) : 'DESC', + ); + + if ( ! empty( $input['search'] ) ) { + $args['s'] = sanitize_text_field( $input['search'] ); + } + + if ( ! empty( $input['include'] ) && is_array( $input['include'] ) ) { + $args['post__in'] = array_map( 'intval', $input['include'] ); + } + + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) { + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- caller-supplied filter on bounded result sets. + $args['post__not_in'] = array_map( 'intval', $input['exclude'] ); + } + + if ( isset( $input['offset'] ) ) { + $args['offset'] = (int) $input['offset']; + } + + return $args; + } + + /** + * Build revision data based on context. + * + * @param \WP_Post $revision Revision post object. + * @param string $context Response context. + * @return array Revision data. + */ + private function build_revision_data( \WP_Post $revision, string $context ): array { + $data = array( + 'id' => $revision->ID, + 'author' => (int) $revision->post_author, + 'date' => $revision->post_date, + 'date_gmt' => $revision->post_date_gmt, + 'guid' => array( + 'rendered' => $revision->guid, + ), + 'modified' => $revision->post_modified, + 'modified_gmt' => $revision->post_modified_gmt, + 'parent' => (int) $revision->post_parent, + 'slug' => $revision->post_name, + 'title' => array( + 'rendered' => get_the_title( $revision->ID ), + ), + ); + + if ( 'edit' === $context ) { + $data['title']['raw'] = $revision->post_title; + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $revision->post_content ), + 'raw' => $revision->post_content, + ); + } elseif ( 'view' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $revision->post_content ), + ); + } elseif ( 'embed' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $revision->post_content ), + ); + } + + return $data; + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'The ID for the parent navigation post', 'airo-wp' ), + 'minimum' => 1, + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to be returned in result set', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Limit results to those matching a string', 'airo-wp' ), + ), + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- schema definition, not a query call. + 'exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes specific revision IDs', 'airo-wp' ), + ), + 'include' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to specific revision IDs', 'airo-wp' ), + ), + 'offset' => array( + 'type' => 'integer', + 'description' => __( 'Offset the result set by a specific number of items', 'airo-wp' ), + 'minimum' => 0, + ), + 'order' => array( + 'type' => 'string', + 'description' => __( 'Order sort attribute ascending or descending', 'airo-wp' ), + 'enum' => array( 'asc', 'desc' ), + 'default' => 'desc', + ), + 'orderby' => array( + 'type' => 'string', + 'description' => __( 'Sort collection by object attribute', 'airo-wp' ), + 'enum' => array( 'date', 'id', 'include', 'relevance', 'slug', 'include_slugs', 'title' ), + 'default' => 'date', + ), + ), + 'required' => array( 'parent' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Navigation revisions list result', 'airo-wp' ), + array( + 'revisions' => array( + 'type' => 'array', + 'description' => __( 'Array of revision objects', 'airo-wp' ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of revisions', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page number', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of items per page', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Navigation/ListNavigations.php b/includes/Mcp/Tools/Navigation/ListNavigations.php new file mode 100644 index 0000000..eb95277 --- /dev/null +++ b/includes/Mcp/Tools/Navigation/ListNavigations.php @@ -0,0 +1,402 @@ + __( 'List Navigations', 'airo-wp' ), + 'description' => __( 'Retrieves a list of navigation posts with filtering and pagination options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array List of navigations with pagination info. + */ + public function execute( array $input ): array { + $args = $this->build_query_args( $input ); + + $query = new \WP_Query( $args ); + + $navigations = array(); + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + if ( $query->have_posts() ) { + while ( $query->have_posts() ) { + $query->the_post(); + $post = get_post(); + + $navigations[] = $this->build_navigation_data( $post, $context ); + } + wp_reset_postdata(); + } + + return array( + 'navigations' => $navigations, + 'total' => (int) $query->found_posts, + 'total_pages' => (int) $query->max_num_pages, + 'page' => (int) ( isset( $input['page'] ) ? $input['page'] : 1 ), + 'per_page' => (int) ( isset( $input['per_page'] ) ? $input['per_page'] : 10 ), + ); + } + + /** + * Build WP_Query arguments from input parameters. + * + * @param array $input Input parameters. + * @return array WP_Query arguments. + */ + private function build_query_args( array $input ): array { + $args = array( + 'post_type' => 'wp_navigation', + 'post_status' => isset( $input['status'] ) ? $input['status'] : 'publish', + 'posts_per_page' => isset( $input['per_page'] ) ? (int) $input['per_page'] : 10, + 'paged' => isset( $input['page'] ) ? (int) $input['page'] : 1, + 'orderby' => isset( $input['orderby'] ) ? $input['orderby'] : 'date', + 'order' => isset( $input['order'] ) ? strtoupper( $input['order'] ) : 'DESC', + ); + + if ( ! empty( $input['search'] ) ) { + $args['s'] = sanitize_text_field( $input['search'] ); + } + + if ( ! empty( $input['search_columns'] ) && is_array( $input['search_columns'] ) ) { + $args['search_columns'] = array_map( 'sanitize_text_field', $input['search_columns'] ); + } + + if ( ! empty( $input['include'] ) && is_array( $input['include'] ) ) { + $args['post__in'] = array_map( 'intval', $input['include'] ); + } + + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) { + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- caller-supplied filter on bounded result sets. + $args['post__not_in'] = array_map( 'intval', $input['exclude'] ); + } + + if ( isset( $input['offset'] ) ) { + $args['offset'] = (int) $input['offset']; + } + + if ( ! empty( $input['slug'] ) ) { + if ( is_array( $input['slug'] ) ) { + $args['post_name__in'] = array_map( 'sanitize_title', $input['slug'] ); + } else { + $args['post_name__in'] = array( sanitize_title( $input['slug'] ) ); + } + } + + $date_query = array(); + + if ( ! empty( $input['after'] ) ) { + $date_query[] = array( + 'after' => $input['after'], + 'inclusive' => true, + 'column' => 'post_date', + ); + } + + if ( ! empty( $input['before'] ) ) { + $date_query[] = array( + 'before' => $input['before'], + 'inclusive' => true, + 'column' => 'post_date', + ); + } + + if ( ! empty( $input['modified_after'] ) ) { + $date_query[] = array( + 'after' => $input['modified_after'], + 'inclusive' => true, + 'column' => 'post_modified', + ); + } + + if ( ! empty( $input['modified_before'] ) ) { + $date_query[] = array( + 'before' => $input['modified_before'], + 'inclusive' => true, + 'column' => 'post_modified', + ); + } + + if ( ! empty( $date_query ) ) { + $args['date_query'] = $date_query; + } + + return $args; + } + + /** + * Build navigation data based on context. + * + * @param \WP_Post $post Post object. + * @param string $context Response context. + * @return array Navigation data. + */ + private function build_navigation_data( \WP_Post $post, string $context ): array { + $data = array( + 'id' => $post->ID, + 'date' => $post->post_date, + 'date_gmt' => $post->post_date_gmt, + 'guid' => array( + 'rendered' => $post->guid, + 'raw' => $post->guid, + ), + 'modified' => $post->post_modified, + 'modified_gmt' => $post->post_modified_gmt, + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'type' => $post->post_type, + 'link' => get_permalink( $post->ID ), + 'title' => array( + 'rendered' => get_the_title( $post->ID ), + 'raw' => $post->post_title, + ), + ); + + if ( 'edit' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + 'raw' => $post->post_content, + ); + $data['template'] = get_page_template_slug( $post->ID ); + } elseif ( 'view' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + ); + $data['template'] = get_page_template_slug( $post->ID ); + } elseif ( 'embed' === $context ) { + $data['content'] = array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + ); + } + + return $data; + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to be returned in result set', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Limit results to those matching a string', 'airo-wp' ), + ), + 'after' => array( + 'type' => 'string', + 'description' => __( 'Limit response to posts published after a given ISO8601 compliant date', 'airo-wp' ), + ), + 'modified_after' => array( + 'type' => 'string', + 'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date', 'airo-wp' ), + ), + 'before' => array( + 'type' => 'string', + 'description' => __( 'Limit response to posts published before a given ISO8601 compliant date', 'airo-wp' ), + ), + 'modified_before' => array( + 'type' => 'string', + 'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date', 'airo-wp' ), + ), + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- schema definition, not a query call. + 'exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes specific IDs', 'airo-wp' ), + ), + 'include' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to specific IDs', 'airo-wp' ), + ), + 'offset' => array( + 'type' => 'integer', + 'description' => __( 'Offset the result set by a specific number of items', 'airo-wp' ), + 'minimum' => 0, + ), + 'order' => array( + 'type' => 'string', + 'description' => __( 'Order sort attribute ascending or descending', 'airo-wp' ), + 'enum' => array( 'asc', 'desc' ), + 'default' => 'desc', + ), + 'orderby' => array( + 'type' => 'string', + 'description' => __( 'Sort collection by post attribute', 'airo-wp' ), + 'enum' => array( 'author', 'date', 'id', 'include', 'modified', 'parent', 'relevance', 'slug', 'include_slugs', 'title' ), + 'default' => 'date', + ), + 'search_columns' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + 'description' => __( 'Array of column names to be searched', 'airo-wp' ), + ), + 'slug' => array( + 'description' => __( 'Limit result set to posts with one or more specific slugs', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Limit result set to posts assigned one or more statuses', 'airo-wp' ), + 'default' => 'publish', + ), + ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'navigations' => array( + 'type' => 'array', + 'description' => __( 'Array of navigation objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the post', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, in the site\'s timezone', 'airo-wp' ), + ), + 'date_gmt' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, as GMT', 'airo-wp' ), + ), + 'guid' => array( + 'type' => 'object', + 'description' => __( 'The globally unique identifier for the post', 'airo-wp' ), + ), + 'modified' => array( + 'type' => 'string', + 'description' => __( 'The date the post was last modified, in the site\'s timezone', 'airo-wp' ), + ), + 'modified_gmt' => array( + 'type' => 'string', + 'description' => __( 'The date the post was last modified, as GMT', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'An alphanumeric identifier for the post unique to its type', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'A named status for the post', 'airo-wp' ), + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Type of post', 'airo-wp' ), + ), + 'link' => array( + 'type' => 'string', + 'description' => __( 'URL to the post', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'object', + 'description' => __( 'The title for the post', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'object', + 'description' => __( 'The content for the post', 'airo-wp' ), + ), + 'template' => array( + 'type' => 'string', + 'description' => __( 'The theme file to use to display the post', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of navigations', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page number', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of items per page', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Navigation/UpdateNavigation.php b/includes/Mcp/Tools/Navigation/UpdateNavigation.php new file mode 100644 index 0000000..ab8b5b9 --- /dev/null +++ b/includes/Mcp/Tools/Navigation/UpdateNavigation.php @@ -0,0 +1,241 @@ + __( 'Update Navigation', 'airo-wp' ), + 'description' => __( 'Updates an existing navigation post', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array + */ + public function execute( array $input ): array { + if ( empty( $input['id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Navigation ID is required', 'airo-wp' ), + ); + } + + $navigation_id = (int) $input['id']; + + $post = get_post( $navigation_id ); + if ( ! $post || 'wp_navigation' !== $post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: Navigation post ID */ + __( 'Navigation with ID %d not found', 'airo-wp' ), + $navigation_id + ), + ); + } + + if ( ! current_user_can( 'edit_post', $navigation_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to update this navigation', 'airo-wp' ), + ); + } + + $post_data = array( + 'ID' => $navigation_id, + ); + + if ( isset( $input['title'] ) ) { + $post_data['post_title'] = sanitize_text_field( $input['title'] ); + } + + if ( isset( $input['content'] ) ) { + $post_data['post_content'] = $input['content']; + } + + if ( isset( $input['slug'] ) ) { + $post_data['post_name'] = sanitize_title( $input['slug'] ); + } + + if ( isset( $input['status'] ) ) { + $post_data['post_status'] = $input['status']; + } + + if ( isset( $input['password'] ) ) { + $post_data['post_password'] = $input['password']; + } + + if ( isset( $input['date'] ) ) { + $post_data['post_date'] = $input['date']; + } + + if ( isset( $input['date_gmt'] ) ) { + $post_data['post_date_gmt'] = $input['date_gmt']; + } + + $result = wp_update_post( $post_data, true ); + + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => $result->get_error_message(), + ); + } + + if ( isset( $input['template'] ) ) { + update_post_meta( $navigation_id, '_wp_page_template', sanitize_text_field( $input['template'] ) ); + } + + $updated_post = get_post( $navigation_id ); + + return array( + 'success' => true, + 'navigation' => $this->build_navigation_data( $updated_post ), + 'message' => __( 'Navigation updated successfully', 'airo-wp' ), + ); + } + + /** + * Build navigation data for response. + * + * @param \WP_Post $post Post object. + * @return array Navigation data. + */ + private function build_navigation_data( \WP_Post $post ): array { + return array( + 'id' => $post->ID, + 'date' => $post->post_date, + 'date_gmt' => $post->post_date_gmt, + 'guid' => array( + 'rendered' => $post->guid, + 'raw' => $post->guid, + ), + 'modified' => $post->post_modified, + 'modified_gmt' => $post->post_modified_gmt, + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'type' => $post->post_type, + 'link' => get_permalink( $post->ID ), + 'title' => array( + 'rendered' => get_the_title( $post->ID ), + 'raw' => $post->post_title, + ), + 'content' => array( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + 'rendered' => apply_filters( 'the_content', $post->post_content ), + 'raw' => $post->post_content, + ), + 'template' => get_page_template_slug( $post->ID ), + ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the post', 'airo-wp' ), + 'minimum' => 1, + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, in the site\'s timezone', 'airo-wp' ), + ), + 'date_gmt' => array( + 'type' => 'string', + 'description' => __( 'The date the post was published, as GMT', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'An alphanumeric identifier for the post unique to its type', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'A named status for the post', 'airo-wp' ), + 'enum' => array( 'publish', 'future', 'draft', 'pending', 'private' ), + ), + 'password' => array( + 'type' => 'string', + 'description' => __( 'A password to protect access to the content and excerpt', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The title for the post', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The content for the post', 'airo-wp' ), + ), + 'template' => array( + 'type' => 'string', + 'description' => __( 'The theme file to use to display the post', 'airo-wp' ), + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Navigation update result', 'airo-wp' ), + array( + 'navigation' => array( + 'type' => 'object', + 'description' => __( 'The updated navigation data', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Pages/CreatePageDraft.php b/includes/Mcp/Tools/Pages/CreatePageDraft.php new file mode 100644 index 0000000..332b8c3 --- /dev/null +++ b/includes/Mcp/Tools/Pages/CreatePageDraft.php @@ -0,0 +1,265 @@ +draft_service = $draft_service; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'edit_pages' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Create Page Draft', 'airo-wp' ), + 'description' => __( 'Creates a draft copy of a published page for safe editing without affecting the live site.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $post_id = isset( $input['post_id'] ) ? (int) $input['post_id'] : 0; + + if ( $post_id <= 0 ) { + return array( + 'success' => false, + 'message' => __( 'A valid post_id is required.', 'airo-wp' ), + ); + } + + $post = get_post( $post_id ); + + if ( ! $post ) { + return array( + 'success' => false, + 'message' => __( 'Post not found.', 'airo-wp' ), + ); + } + + if ( 'page' !== $post->post_type ) { + return array( + 'success' => false, + 'message' => __( 'Drafts can only be created for pages (post_type=page).', 'airo-wp' ), + ); + } + + if ( 'publish' !== $post->post_status ) { + return array( + 'success' => false, + 'message' => __( 'A draft can only be created from a published page.', 'airo-wp' ), + ); + } + + if ( $this->draft_service->has_draft( $post_id ) ) { + return array( + 'success' => false, + 'message' => __( 'A draft version already exists for this page.', 'airo-wp' ), + ); + } + + $draft_data = array( + 'post_title' => sanitize_text_field( isset( $input['title'] ) ? $input['title'] : $post->post_title ), + // All MCP callers are admins who have unfiltered_html; wp_insert_post() skips kses for them. + 'post_content' => wp_kses_post( isset( $input['content'] ) ? $input['content'] : $post->post_content ), + 'post_excerpt' => sanitize_textarea_field( isset( $input['excerpt'] ) ? $input['excerpt'] : $post->post_excerpt ), + 'post_status' => 'draft', + 'post_type' => 'page', + 'post_author' => get_current_user_id(), + 'post_parent' => $post_id, + 'menu_order' => $post->menu_order, + 'post_password' => $post->post_password, + 'comment_status' => $post->comment_status, + 'ping_status' => $post->ping_status, + 'post_content_filtered' => $post->post_content_filtered, + ); + + $draft_id = wp_insert_post( wp_slash( $draft_data ), true ); + + if ( is_wp_error( $draft_id ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Failed to create draft: %s', 'airo-wp' ), + $draft_id->get_error_message() + ), + ); + } + + // Generate unique slug. + $base_slug = sanitize_title( $post->post_name . '-draft' ); + $slug = wp_unique_post_slug( $base_slug, $draft_id, 'draft', 'page', $post_id ); + + global $wpdb; + + if ( empty( $slug ) ) { + $slug = $base_slug; + $counter = 2; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + while ( $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND ID != %d LIMIT 1", $slug, $draft_id ) ) ) { + $slug = $base_slug . '-' . $counter; + ++$counter; + } + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $updated = $wpdb->update( + $wpdb->posts, + array( 'post_name' => $slug ), + array( 'ID' => $draft_id ), + array( '%s' ), + array( '%d' ) + ); + + if ( false === $updated ) { + wp_delete_post( $draft_id, true ); + + return array( + 'success' => false, + 'message' => __( 'Failed to set draft slug.', 'airo-wp' ), + ); + } + + clean_post_cache( $draft_id ); + + $this->draft_service->copy_post_meta( $post_id, $draft_id ); + + // Copy thumbnail if exists. + $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true ); + + if ( ! empty( $thumbnail_id ) ) { + update_post_meta( $draft_id, '_thumbnail_id', $thumbnail_id ); + } + + // Set draft meta relationships. + update_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, $post_id ); + update_post_meta( $draft_id, DraftPageService::META_DRAFT_CREATED, time() ); + update_post_meta( $post_id, DraftPageService::META_HAS_DRAFT, $draft_id ); + + /** + * Fires after a page draft has been created. + * + * @param int $draft_id The new draft post ID. + * @param int $post_id The original published page ID. + */ + do_action( 'airowp_draft_created', $draft_id, $post_id ); + + return array( + 'success' => true, + 'message' => __( 'Draft created successfully.', 'airo-wp' ), + 'draft_id' => $draft_id, + 'edit_url' => get_edit_post_link( $draft_id, 'raw' ), + 'original_id' => $post_id, + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'The ID of the published page to create a draft of.', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'Optional title override for the draft.', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'Optional content override for the draft.', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'Optional excerpt override for the draft.', 'airo-wp' ), + ), + ), + 'required' => array( 'post_id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Page draft creation result', 'airo-wp' ), + array( + 'draft_id' => array( + 'type' => 'integer', + 'description' => __( 'The new draft post ID', 'airo-wp' ), + ), + 'edit_url' => array( + 'type' => 'string', + 'description' => __( 'URL to edit the draft', 'airo-wp' ), + ), + 'original_id' => array( + 'type' => 'integer', + 'description' => __( 'The original published page ID', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Pages/DeletePageRevision.php b/includes/Mcp/Tools/Pages/DeletePageRevision.php new file mode 100644 index 0000000..d6c42ed --- /dev/null +++ b/includes/Mcp/Tools/Pages/DeletePageRevision.php @@ -0,0 +1,224 @@ + __( 'Delete Page Revision', 'airo-wp' ), + 'description' => __( 'Deletes a specific page revision by ID. Revisions are permanently deleted and do not support trash.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Deletion result. + */ + public function execute( array $input ): array { + // Load revision functions. + $this->load_admin_file( 'post.php' ); + + if ( empty( $input['parent'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Parent page ID is required', 'airo-wp' ), + ); + } + + if ( empty( $input['id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Revision ID is required', 'airo-wp' ), + ); + } + + $parent_id = (int) $input['parent']; + $revision_id = (int) $input['id']; + + // Check if parent post exists and is a page. + $parent_post = get_post( $parent_id ); + if ( ! $parent_post || 'page' !== $parent_post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: page ID */ + __( 'Page with ID %d not found', 'airo-wp' ), + $parent_id + ), + ); + } + + // Get the revision. + $revision = wp_get_post_revision( $revision_id ); + if ( ! $revision ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: revision ID */ + __( 'Revision with ID %d not found', 'airo-wp' ), + $revision_id + ), + ); + } + + // Verify the revision belongs to the parent page. + if ( (int) $revision->post_parent !== $parent_id ) { + return array( + 'success' => false, + 'message' => __( 'Revision does not belong to the specified parent page', 'airo-wp' ), + ); + } + + // Check permissions. + if ( ! current_user_can( 'delete_post', $parent_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to delete revisions for this page', 'airo-wp' ), + ); + } + + // Store revision data before deletion. + $deleted_data = array( + 'id' => $revision->ID, + 'author_id' => (int) $revision->post_author, + 'date_created' => $revision->post_date, + 'date_modified' => $revision->post_modified, + 'parent_id' => (int) $revision->post_parent, + 'title' => $revision->post_title, + ); + + // Delete the revision permanently. + $result = wp_delete_post_revision( $revision_id ); + + if ( false === $result || is_wp_error( $result ) ) { + $error_message = is_wp_error( $result ) + ? $result->get_error_message() + : __( 'Failed to delete revision', 'airo-wp' ); + return array( + 'success' => false, + 'message' => $error_message, + ); + } + + return array( + 'success' => true, + 'revision_id' => $revision_id, + 'message' => __( 'Revision deleted successfully', 'airo-wp' ), + 'deleted' => $deleted_data, + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the parent page for the revision', 'airo-wp' ), + 'minimum' => 1, + ), + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the revision to delete', 'airo-wp' ), + 'minimum' => 1, + ), + 'force' => array( + 'type' => 'boolean', + 'description' => __( 'Required to be true, as revisions do not support trashing', 'airo-wp' ), + 'default' => true, + ), + ), + 'required' => array( 'parent', 'id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Page revision deletion result', 'airo-wp' ), + array( + 'revision_id' => array( + 'type' => 'integer', + 'description' => __( 'The deleted revision ID', 'airo-wp' ), + ), + 'deleted' => array( + 'type' => 'object', + 'description' => __( 'The deleted revision data', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The revision ID', 'airo-wp' ), + ), + 'parent_id' => array( + 'type' => 'integer', + 'description' => __( 'The parent page ID', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The revision author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The revision creation date', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The revision title', 'airo-wp' ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Pages/DiscardPageDraft.php b/includes/Mcp/Tools/Pages/DiscardPageDraft.php new file mode 100644 index 0000000..c5cae3c --- /dev/null +++ b/includes/Mcp/Tools/Pages/DiscardPageDraft.php @@ -0,0 +1,181 @@ +draft_service = $draft_service; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'edit_pages' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Discard Page Draft', 'airo-wp' ), + 'description' => __( 'Discards a draft page by deleting it without merging changes into the original published page.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $draft_id = absint( $input['draft_id'] ?? 0 ); + + if ( $draft_id <= 0 ) { + return array( + 'success' => false, + 'message' => __( 'A valid draft page ID is required.', 'airo-wp' ), + ); + } + + $draft = get_post( $draft_id ); + + if ( ! $draft ) { + return array( + 'success' => false, + 'message' => __( 'The requested draft page does not exist.', 'airo-wp' ), + ); + } + + if ( 'page' !== $draft->post_type ) { + return array( + 'success' => false, + 'message' => __( 'Only page drafts can be published.', 'airo-wp' ), + ); + } + + if ( 'draft' !== $draft->post_status ) { + return array( + 'success' => false, + 'message' => __( 'Only posts in draft status can be published as a Site Designer draft.', 'airo-wp' ), + ); + } + + $original_meta = get_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, true ); + + if ( empty( $original_meta ) ) { + return array( + 'success' => false, + 'message' => __( 'The page is not a Site Designer draft copy.', 'airo-wp' ), + ); + } + + $original_id = absint( $original_meta ); + + if ( $original_id <= 0 ) { + return array( + 'success' => false, + 'message' => __( 'The draft is missing a valid original page reference.', 'airo-wp' ), + ); + } + + $force_delete = array_key_exists( 'force_delete', $input ) ? (bool) $input['force_delete'] : false; + $result = $this->draft_service->discard_draft( $draft_id, $force_delete ); + + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => $result->get_error_message(), + ); + } + + return array( + 'success' => true, + 'original_id' => $original_id, + 'message' => __( 'Draft discarded successfully.', 'airo-wp' ), + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'draft_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'The ID of the draft page to discard.', 'airo-wp' ), + ), + 'force_delete' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to permanently delete the draft (true) or move it to trash so it can be restored (false). Defaults to false.', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'draft_id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Page draft discard result', 'airo-wp' ), + array( + 'original_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the original page that was updated.', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Pages/GetPageDraftStatus.php b/includes/Mcp/Tools/Pages/GetPageDraftStatus.php new file mode 100644 index 0000000..c9a5d59 --- /dev/null +++ b/includes/Mcp/Tools/Pages/GetPageDraftStatus.php @@ -0,0 +1,193 @@ +draft_service = $draft_service; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'edit_pages' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Get Page Draft Status', 'airo-wp' ), + 'description' => __( 'Returns draft relationship information for a page: whether it is a draft, has a draft, or can have one created.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $post_id = (int) ( $input['post_id'] ?? 0 ); + + $post = get_post( $post_id ); + + if ( ! $post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: page ID */ + __( 'Page with ID %d not found.', 'airo-wp' ), + $post_id + ), + ); + } + + if ( 'page' !== $post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d is not a page.', 'airo-wp' ), + $post_id + ), + ); + } + + // Check if this page IS a draft. + $draft_of_meta = get_post_meta( $post_id, DraftPageService::META_DRAFT_OF, true ); + $original_id = absint( $draft_of_meta ); + + if ( $original_id > 0 && 'draft' === $post->post_status ) { + return array( + 'success' => true, + 'is_draft' => true, + 'has_draft' => false, + 'draft_id' => $post_id, + 'original_id' => $original_id, + 'can_create' => false, + ); + } + + // Check if this page HAS a draft. + $draft = $this->draft_service->get_draft( $post_id ); + + if ( null !== $draft ) { + return array( + 'success' => true, + 'is_draft' => false, + 'has_draft' => true, + 'draft_id' => $draft->ID, + 'original_id' => $post_id, + 'can_create' => false, + ); + } + + // Otherwise: check if a draft can be created. + $can_create = 'page' === $post->post_type && 'publish' === $post->post_status; + + return array( + 'success' => true, + 'is_draft' => false, + 'has_draft' => false, + 'draft_id' => null, + 'original_id' => $post_id, + 'can_create' => $can_create, + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'The page ID to check draft status for.', 'airo-wp' ), + ), + ), + 'required' => array( 'post_id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Page draft status result', 'airo-wp' ), + array( + 'is_draft' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the page is itself a draft copy.', 'airo-wp' ), + ), + 'has_draft' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the page has a draft copy.', 'airo-wp' ), + ), + 'draft_id' => array( + 'type' => array( 'integer', 'null' ), + 'description' => __( 'The draft page ID, if applicable.', 'airo-wp' ), + ), + 'original_id' => array( + 'type' => array( 'integer', 'null' ), + 'description' => __( 'The original page ID, if applicable.', 'airo-wp' ), + ), + 'can_create' => array( + 'type' => 'boolean', + 'description' => __( 'Whether a draft can be created for this page.', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Pages/GetPageRevision.php b/includes/Mcp/Tools/Pages/GetPageRevision.php new file mode 100644 index 0000000..30fac66 --- /dev/null +++ b/includes/Mcp/Tools/Pages/GetPageRevision.php @@ -0,0 +1,235 @@ + __( 'Get Page Revision', 'airo-wp' ), + 'description' => __( 'Retrieves a specific page revision by ID', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Revision data or error. + */ + public function execute( array $input ): array { + if ( empty( $input['parent'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Parent page ID is required', 'airo-wp' ), + ); + } + + if ( empty( $input['id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Revision ID is required', 'airo-wp' ), + ); + } + + $parent_id = (int) $input['parent']; + $revision_id = (int) $input['id']; + + // Check if parent post exists and is a page. + $parent_post = get_post( $parent_id ); + if ( ! $parent_post || 'page' !== $parent_post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: page ID */ + __( 'Page with ID %d not found', 'airo-wp' ), + $parent_id + ), + ); + } + + // Get the revision. + $revision = wp_get_post_revision( $revision_id ); + if ( ! $revision ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: revision ID */ + __( 'Revision with ID %d not found', 'airo-wp' ), + $revision_id + ), + ); + } + + // Verify the revision belongs to the parent page. + if ( (int) $revision->post_parent !== $parent_id ) { + return array( + 'success' => false, + 'message' => __( 'Revision does not belong to the specified parent page', 'airo-wp' ), + ); + } + + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + return $this->format_revision( $revision, $context ); + } + + /** + * Format revision data based on context. + * + * @param \WP_Post $revision Revision post object. + * @param string $context Context (view, embed, edit). + * @return array Formatted revision data. + */ + private function format_revision( \WP_Post $revision, string $context ): array { + $data = array( + 'id' => $revision->ID, + 'author_id' => (int) $revision->post_author, + 'date_created' => $revision->post_date, + 'date_modified' => $revision->post_modified, + 'parent_id' => (int) $revision->post_parent, + 'slug' => $revision->post_name, + ); + + switch ( $context ) { + case 'embed': + $data['title'] = $revision->post_title; + $data['excerpt'] = wp_trim_words( $revision->post_excerpt ? $revision->post_excerpt : $revision->post_content, 55 ); + break; + + case 'edit': + $data['title'] = $revision->post_title; + $data['content'] = $revision->post_content; + $data['excerpt'] = $revision->post_excerpt; + break; + + case 'view': + default: + $data['title'] = $revision->post_title; + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + $data['content'] = apply_filters( 'the_content', $revision->post_content ); + $data['excerpt'] = $revision->post_excerpt; + break; + } + + return $data; + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the parent page for the revision', 'airo-wp' ), + 'minimum' => 1, + ), + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Unique identifier for the revision', 'airo-wp' ), + 'minimum' => 1, + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + ), + 'required' => array( 'parent', 'id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + public function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The revision ID', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The revision author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The revision creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The revision modification date', 'airo-wp' ), + ), + 'parent_id' => array( + 'type' => 'integer', + 'description' => __( 'The parent page ID', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The revision slug', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The revision title', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The revision content', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The revision excerpt', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Pages/ListPageRevisions.php b/includes/Mcp/Tools/Pages/ListPageRevisions.php new file mode 100644 index 0000000..a4b0e01 --- /dev/null +++ b/includes/Mcp/Tools/Pages/ListPageRevisions.php @@ -0,0 +1,369 @@ + __( 'List Page Revisions', 'airo-wp' ), + 'description' => __( 'Retrieves a list of revisions for a page with filtering and pagination options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array List of revisions or error. + */ + public function execute( array $input ): array { + if ( empty( $input['parent'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Parent page ID is required', 'airo-wp' ), + ); + } + + $parent_id = (int) $input['parent']; + + // Check if parent post exists. + $parent_post = get_post( $parent_id ); + if ( ! $parent_post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d not found', 'airo-wp' ), + $parent_id + ), + ); + } + + // Verify the parent is a page. + if ( 'page' !== $parent_post->post_type ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d is not a page', 'airo-wp' ), + $parent_id + ), + ); + } + + // Build WP_Query args so pagination and filtering happen at the DB layer. + $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 10; + $page = isset( $input['page'] ) ? (int) $input['page'] : 1; + $use_offset = isset( $input['offset'] ); + $offset = $use_offset ? (int) $input['offset'] : 0; + $has_search = ! empty( $input['search'] ); + + $args = array( + 'post_type' => 'revision', + 'post_parent' => $parent_id, + 'post_status' => 'inherit', + 'posts_per_page' => $per_page, + 'orderby' => $this->map_orderby( isset( $input['orderby'] ) ? $input['orderby'] : 'date', $has_search ), + 'order' => isset( $input['order'] ) ? strtoupper( $input['order'] ) : 'DESC', + ); + + // `offset` and `paged` are mutually exclusive in WP_Query — combining them + // silently breaks found_posts / max_num_pages. Honor offset exclusively + // when provided, otherwise use page-based pagination. + if ( $use_offset ) { + $args['offset'] = $offset; + } else { + $args['paged'] = $page; + } + + if ( $has_search ) { + $args['s'] = sanitize_text_field( $input['search'] ); + } + if ( ! empty( $input['include'] ) && is_array( $input['include'] ) ) { + $args['post__in'] = array_map( 'intval', $input['include'] ); + } + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) { + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- caller-supplied filter on bounded revision sets. + $args['post__not_in'] = array_map( 'intval', $input['exclude'] ); + } + + $query = new \WP_Query( $args ); + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + $revision_data = array(); + if ( $query->have_posts() ) { + while ( $query->have_posts() ) { + $query->the_post(); + $revision_data[] = $this->format_revision( get_post(), $context ); + } + wp_reset_postdata(); + } + + // Report the effective page so the response is internally consistent + // regardless of which pagination mode the caller used. + $effective_page = $use_offset + ? ( (int) floor( $offset / max( 1, $per_page ) ) + 1 ) + : $page; + + return array( + 'revisions' => $revision_data, + 'total' => (int) $query->found_posts, + 'total_pages' => (int) $query->max_num_pages, + 'page' => $effective_page, + 'per_page' => $per_page, + ); + } + + /** + * Translate the schema's public `orderby` enum to a WP_Query-native key. + * + * @param string $orderby Raw orderby value from input. + * @param bool $has_search Whether the query carries a search term. + * @return string WP_Query-compatible orderby value. + */ + private function map_orderby( string $orderby, bool $has_search ): string { + $map = array( + 'date' => 'date', + 'id' => 'ID', + 'slug' => 'name', + 'title' => 'title', + 'include' => 'post__in', + 'include_slugs' => 'post_name__in', + 'relevance' => 'relevance', + ); + + $mapped = isset( $map[ $orderby ] ) ? $map[ $orderby ] : 'date'; + + if ( 'relevance' === $mapped && ! $has_search ) { + return 'date'; + } + + return $mapped; + } + + /** + * Format revision data based on context. + * + * @param \WP_Post $revision Revision post object. + * @param string $context Context (view, embed, edit). + * @return array Formatted revision data. + */ + private function format_revision( \WP_Post $revision, string $context ): array { + $data = array( + 'id' => $revision->ID, + 'author_id' => (int) $revision->post_author, + 'date_created' => $revision->post_date, + 'date_modified' => $revision->post_modified, + 'parent_id' => (int) $revision->post_parent, + 'slug' => $revision->post_name, + ); + + switch ( $context ) { + case 'embed': + $data['title'] = $revision->post_title; + $data['excerpt'] = wp_trim_words( $revision->post_excerpt ? $revision->post_excerpt : $revision->post_content, 55 ); + break; + + case 'edit': + $data['title'] = $revision->post_title; + $data['content'] = $revision->post_content; + $data['excerpt'] = $revision->post_excerpt; + break; + + case 'view': + default: + $data['title'] = $revision->post_title; + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + $data['content'] = apply_filters( 'the_content', $revision->post_content ); + $data['excerpt'] = $revision->post_excerpt; + break; + } + + return $data; + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the page for the revisions', 'airo-wp' ), + 'minimum' => 1, + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to be returned in result set', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Limit results to those matching a string', 'airo-wp' ), + ), + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- schema definition, not a query call. + 'exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes specific revision IDs', 'airo-wp' ), + ), + 'include' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to specific revision IDs', 'airo-wp' ), + ), + 'offset' => array( + 'type' => 'integer', + 'description' => __( 'Offset the result set by a specific number of items. When provided, takes precedence over `page` — the two are mutually exclusive in WP_Query and combining them breaks pagination totals.', 'airo-wp' ), + 'minimum' => 0, + ), + 'order' => array( + 'type' => 'string', + 'description' => __( 'Order sort attribute ascending or descending', 'airo-wp' ), + 'enum' => array( 'asc', 'desc' ), + 'default' => 'desc', + ), + 'orderby' => array( + 'type' => 'string', + 'description' => __( 'Sort collection by object attribute', 'airo-wp' ), + 'enum' => array( 'date', 'id', 'include', 'relevance', 'slug', 'include_slugs', 'title' ), + 'default' => 'date', + ), + ), + 'required' => array( 'parent' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + public function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'revisions' => array( + 'type' => 'array', + 'description' => __( 'Array of revision objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The revision ID', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The revision author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The revision creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The revision modification date', 'airo-wp' ), + ), + 'parent_id' => array( + 'type' => 'integer', + 'description' => __( 'The parent page ID', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The revision slug', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The revision title', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The revision content', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The revision excerpt', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of revisions', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page number', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of items per page', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Pages/PublishPageDraft.php b/includes/Mcp/Tools/Pages/PublishPageDraft.php new file mode 100644 index 0000000..2e54f5b --- /dev/null +++ b/includes/Mcp/Tools/Pages/PublishPageDraft.php @@ -0,0 +1,181 @@ +draft_service = $draft_service; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'edit_pages' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Publish Page Draft', 'airo-wp' ), + 'description' => __( 'Publishes a draft page by merging its content into the original published page and deleting the draft.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $draft_id = absint( $input['draft_id'] ?? 0 ); + + if ( $draft_id <= 0 ) { + return array( + 'success' => false, + 'message' => __( 'A valid draft page ID is required.', 'airo-wp' ), + ); + } + + $draft = get_post( $draft_id ); + + if ( ! $draft ) { + return array( + 'success' => false, + 'message' => __( 'The requested draft page does not exist.', 'airo-wp' ), + ); + } + + if ( 'page' !== $draft->post_type ) { + return array( + 'success' => false, + 'message' => __( 'Only page drafts can be published.', 'airo-wp' ), + ); + } + + if ( 'draft' !== $draft->post_status ) { + return array( + 'success' => false, + 'message' => __( 'Only posts in draft status can be published as a Site Designer draft.', 'airo-wp' ), + ); + } + + $original_meta = get_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, true ); + + if ( empty( $original_meta ) ) { + return array( + 'success' => false, + 'message' => __( 'The page is not a Site Designer draft copy.', 'airo-wp' ), + ); + } + + $original_id = absint( $original_meta ); + + if ( $original_id <= 0 ) { + return array( + 'success' => false, + 'message' => __( 'The draft is missing a valid original page reference.', 'airo-wp' ), + ); + } + + $force_delete = array_key_exists( 'force_delete', $input ) ? (bool) $input['force_delete'] : false; + $result = $this->draft_service->publish_draft( $draft_id, $force_delete ); + + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => $result->get_error_message(), + ); + } + + return array( + 'success' => true, + 'original_id' => $original_id, + 'message' => __( 'Draft published successfully.', 'airo-wp' ), + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'draft_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'The ID of the draft page to publish.', 'airo-wp' ), + ), + 'force_delete' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to permanently delete the draft (true) or move it to trash so it can be restored (false). Defaults to false.', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'draft_id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Page draft publish result', 'airo-wp' ), + array( + 'original_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the original page that was updated.', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Plugins/ActivatePlugin.php b/includes/Mcp/Tools/Plugins/ActivatePlugin.php new file mode 100644 index 0000000..55487d3 --- /dev/null +++ b/includes/Mcp/Tools/Plugins/ActivatePlugin.php @@ -0,0 +1,227 @@ + __( 'Activate Plugin', 'airo-wp' ), + 'description' => __( 'Installs a plugin from WordPress.org if not present, then activates it', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'plugin_slug' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug (e.g. "hello-dolly")', 'airo-wp' ), + ), + ), + 'required' => array( 'plugin_slug' ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Plugin activation result', 'airo-wp' ), + array( + 'plugin' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug', 'airo-wp' ), + ), + 'version' => array( + 'type' => 'string', + 'description' => __( 'The activated plugin version', 'airo-wp' ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'plugin-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $slug = isset( $input['plugin_slug'] ) ? sanitize_text_field( $input['plugin_slug'] ) : ''; + + if ( empty( $slug ) ) { + return array( + 'success' => false, + 'message' => __( 'Plugin slug is required.', 'airo-wp' ), + ); + } + + if ( ! current_user_can( 'install_plugins' ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to install plugins.', 'airo-wp' ), + ); + } + + $this->load_admin_file( 'plugin.php' ); + + $plugin_file = $this->find_plugin_file( $slug ); + + if ( ! $plugin_file ) { + $install_result = $this->install_from_org( $slug ); + + if ( is_wp_error( $install_result ) ) { + return array( + 'success' => false, + 'message' => $install_result->get_error_message(), + 'plugin' => $slug, + ); + } + + $plugin_file = $this->find_plugin_file( $slug ); + + if ( ! $plugin_file ) { + return array( + 'success' => false, + 'message' => __( 'Plugin installed but could not locate plugin file.', 'airo-wp' ), + 'plugin' => $slug, + ); + } + } + + if ( is_plugin_active( $plugin_file ) ) { + return array( + 'success' => true, + 'message' => __( 'Plugin is already active.', 'airo-wp' ), + 'plugin' => $slug, + 'version' => $this->get_plugin_version( $plugin_file ), + ); + } + + $result = activate_plugin( $plugin_file ); + + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => $result->get_error_message(), + 'plugin' => $slug, + ); + } + + return array( + 'success' => true, + 'message' => __( 'Plugin activated successfully.', 'airo-wp' ), + 'plugin' => $slug, + 'version' => $this->get_plugin_version( $plugin_file ), + ); + } + + /** + * Install a plugin from WordPress.org. + * + * @param string $slug Plugin slug. + * @return true|\WP_Error True on success, WP_Error on failure. + */ + private function install_from_org( string $slug ) { + $this->load_admin_file( 'plugin-install.php' ); + $this->load_admin_file( 'file.php' ); + $this->load_admin_file( 'class-wp-upgrader.php' ); + + $api = plugins_api( + 'plugin_information', + array( + 'slug' => $slug, + 'fields' => array( 'sections' => false ), + ) + ); + + if ( is_wp_error( $api ) ) { + return $api; + } + + $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + if ( ! $result ) { + return new \WP_Error( 'install_failed', __( 'Plugin installation failed.', 'airo-wp' ) ); + } + + return true; + } + + /** + * Find the main plugin file for a given slug. + * + * @param string $slug Plugin slug. + * @return string|false Plugin file relative path or false if not found. + */ + private function find_plugin_file( string $slug ) { + $plugins = get_plugins(); + + // Try the conventional path first. + $conventional = $slug . '/' . $slug . '.php'; + if ( isset( $plugins[ $conventional ] ) ) { + return $conventional; + } + + // Search by directory name. + foreach ( $plugins as $file => $data ) { + if ( strpos( $file, $slug . '/' ) === 0 ) { + return $file; + } + } + + return false; + } + + /** + * Get the version of a plugin by its file path. + * + * @param string $file Plugin file relative path. + * @return string Plugin version or empty string. + */ + private function get_plugin_version( string $file ): string { + $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); + + return isset( $data['Version'] ) ? $data['Version'] : ''; + } +} diff --git a/includes/Mcp/Tools/Plugins/DeactivatePlugin.php b/includes/Mcp/Tools/Plugins/DeactivatePlugin.php new file mode 100644 index 0000000..cb88baa --- /dev/null +++ b/includes/Mcp/Tools/Plugins/DeactivatePlugin.php @@ -0,0 +1,185 @@ + __( 'Deactivate Plugin', 'airo-wp' ), + 'description' => __( 'Deactivates a plugin by slug, optionally uninstalling it', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'plugin_slug' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug (e.g. "hello-dolly")', 'airo-wp' ), + ), + 'uninstall' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to also uninstall (delete) the plugin', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'plugin_slug' ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Plugin deactivation result', 'airo-wp' ), + array( + 'plugin' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug', 'airo-wp' ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'plugin-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $slug = isset( $input['plugin_slug'] ) ? sanitize_text_field( $input['plugin_slug'] ) : ''; + + if ( empty( $slug ) ) { + return array( + 'success' => false, + 'message' => __( 'Plugin slug is required.', 'airo-wp' ), + ); + } + + $this->load_admin_file( 'plugin.php' ); + + $plugin_file = $this->find_plugin_file( $slug ); + + if ( ! $plugin_file ) { + return array( + 'success' => false, + 'message' => __( 'Plugin not found.', 'airo-wp' ), + 'plugin' => $slug, + ); + } + + if ( is_plugin_active( $plugin_file ) ) { + deactivate_plugins( $plugin_file ); + } + + $uninstall = ! empty( $input['uninstall'] ); + + if ( $uninstall ) { + $uninstall_result = $this->uninstall_plugin( $plugin_file, $slug ); + + if ( is_wp_error( $uninstall_result ) ) { + return array( + 'success' => false, + 'message' => $uninstall_result->get_error_message(), + 'plugin' => $slug, + ); + } + + return array( + 'success' => true, + 'message' => __( 'Plugin deactivated and uninstalled successfully.', 'airo-wp' ), + 'plugin' => $slug, + ); + } + + return array( + 'success' => true, + 'message' => __( 'Plugin deactivated successfully.', 'airo-wp' ), + 'plugin' => $slug, + ); + } + + /** + * Find the main plugin file for a given slug. + * + * @param string $slug Plugin slug. + * @return string|false Plugin file relative path or false if not found. + */ + private function find_plugin_file( string $slug ) { + $plugins = get_plugins(); + + // Try the conventional path first. + $conventional = $slug . '/' . $slug . '.php'; + if ( isset( $plugins[ $conventional ] ) ) { + return $conventional; + } + + // Search by directory name. + foreach ( $plugins as $file => $data ) { + if ( strpos( $file, $slug . '/' ) === 0 ) { + return $file; + } + } + + return false; + } + + /** + * Uninstall (delete) a plugin. + * + * @param string $file Plugin file relative path. + * @param string $_slug Plugin slug (reserved for future use). + * @return true|\WP_Error True on success, WP_Error on failure. + */ + private function uninstall_plugin( string $file, string $_slug ) { + if ( ! current_user_can( 'delete_plugins' ) ) { + return new \WP_Error( 'permission_denied', __( 'You do not have permission to delete plugins.', 'airo-wp' ) ); + } + + $this->load_admin_file( 'file.php' ); + + $result = delete_plugins( array( $file ) ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + return true; + } +} diff --git a/includes/Mcp/Tools/Plugins/GetPlugin.php b/includes/Mcp/Tools/Plugins/GetPlugin.php new file mode 100644 index 0000000..20287b1 --- /dev/null +++ b/includes/Mcp/Tools/Plugins/GetPlugin.php @@ -0,0 +1,193 @@ +plugin_helper = $plugin_helper; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'activate_plugins' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Get Plugin', 'airo-wp' ), + 'description' => __( 'Retrieves information about a specific WordPress plugin by its slug, including current version and available-update info (whether an update exists and the target version)', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'plugin_slug' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug (e.g. "hello-dolly")', 'airo-wp' ), + 'minLength' => 1, + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Response context: view, embed, or edit', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'force_check' => array( + 'type' => 'boolean', + 'description' => __( 'Force a refresh of available plugin updates from WordPress.org before reading. Defaults to false; the cached transient is normally kept fresh by core cron.', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'plugin_slug' ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Plugin information', 'airo-wp' ), + array( + 'plugin' => array( + 'type' => 'object', + 'description' => __( 'Plugin details', 'airo-wp' ), + 'properties' => array( + 'slug' => array( 'type' => 'string' ), + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + 'author' => array( 'type' => 'string' ), + 'description' => array( 'type' => 'string' ), + 'plugin_uri' => array( 'type' => 'string' ), + 'author_uri' => array( 'type' => 'string' ), + 'text_domain' => array( 'type' => 'string' ), + 'status' => array( 'type' => 'string' ), + 'file' => array( 'type' => 'string' ), + 'network_only' => array( 'type' => 'boolean' ), + 'requires_wp' => array( 'type' => 'string' ), + 'requires_php' => array( 'type' => 'string' ), + 'update_available' => array( 'type' => 'boolean' ), + 'new_version' => array( 'type' => array( 'string', 'null' ) ), + ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'plugin-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $slug = isset( $input['plugin_slug'] ) ? sanitize_text_field( $input['plugin_slug'] ) : ''; + + if ( empty( $slug ) ) { + return array( + 'success' => false, + 'message' => __( 'Plugin slug is required.', 'airo-wp' ), + ); + } + + $context = isset( $input['context'] ) ? sanitize_text_field( $input['context'] ) : 'view'; + + if ( ! in_array( $context, array( 'view', 'embed', 'edit' ), true ) ) { + $context = 'view'; + } + + $this->load_admin_file( 'plugin.php' ); + + $this->plugin_helper->refresh_updates( ! empty( $input['force_check'] ) ); + + $plugin_file = $this->plugin_helper->find_file( $slug ); + + if ( ! $plugin_file ) { + return array( + 'success' => false, + 'message' => __( 'Plugin not found.', 'airo-wp' ), + ); + } + + $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false ); + $active = is_plugin_active( $plugin_file ); + + if ( 'embed' === $context ) { + return array( + 'success' => true, + 'plugin' => array( + 'slug' => $slug, + 'name' => isset( $data['Name'] ) ? $data['Name'] : '', + 'version' => isset( $data['Version'] ) ? $data['Version'] : '', + 'status' => $active ? 'active' : 'inactive', + ), + ); + } + + $update_info = $this->plugin_helper->get_update_info( $plugin_file ); + + return array( + 'success' => true, + 'plugin' => array( + 'slug' => $slug, + 'name' => isset( $data['Name'] ) ? $data['Name'] : '', + 'version' => isset( $data['Version'] ) ? $data['Version'] : '', + 'author' => isset( $data['Author'] ) ? $data['Author'] : '', + 'description' => isset( $data['Description'] ) ? $data['Description'] : '', + 'plugin_uri' => isset( $data['PluginURI'] ) ? $data['PluginURI'] : '', + 'author_uri' => isset( $data['AuthorURI'] ) ? $data['AuthorURI'] : '', + 'text_domain' => isset( $data['TextDomain'] ) ? $data['TextDomain'] : '', + 'status' => $active ? 'active' : 'inactive', + 'file' => $plugin_file, + 'network_only' => ! empty( $data['Network'] ), + 'requires_wp' => isset( $data['RequiresWP'] ) ? $data['RequiresWP'] : '', + 'requires_php' => isset( $data['RequiresPHP'] ) ? $data['RequiresPHP'] : '', + 'update_available' => $update_info['update_available'], + 'new_version' => $update_info['new_version'], + ), + ); + } +} diff --git a/includes/Mcp/Tools/Plugins/ListPlugins.php b/includes/Mcp/Tools/Plugins/ListPlugins.php new file mode 100644 index 0000000..05b202b --- /dev/null +++ b/includes/Mcp/Tools/Plugins/ListPlugins.php @@ -0,0 +1,222 @@ +plugin_helper = $plugin_helper; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'activate_plugins' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'List Plugins', 'airo-wp' ), + 'description' => __( 'Retrieves a list of installed WordPress plugins with their status, metadata, and available-update info (current version plus the new version when an update is available)', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'context' => array( + 'type' => 'string', + 'description' => __( 'Response context: view, embed, or edit', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Search term to filter plugins by name or description', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Filter by plugin status', 'airo-wp' ), + 'enum' => array( 'active', 'inactive' ), + ), + 'force_check' => array( + 'type' => 'boolean', + 'description' => __( 'Force a refresh of available plugin updates from WordPress.org before reading. Defaults to false; the cached transient is normally kept fresh by core cron.', 'airo-wp' ), + 'default' => false, + ), + ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Plugin list result', 'airo-wp' ), + array( + 'plugins' => array( + 'type' => 'array', + 'description' => __( 'List of plugins', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'slug' => array( 'type' => 'string' ), + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + 'author' => array( 'type' => 'string' ), + 'description' => array( 'type' => 'string' ), + 'status' => array( 'type' => 'string' ), + 'file' => array( 'type' => 'string' ), + 'plugin_uri' => array( 'type' => 'string' ), + 'author_uri' => array( 'type' => 'string' ), + 'text_domain' => array( 'type' => 'string' ), + 'network_only' => array( 'type' => 'boolean' ), + 'requires_wp' => array( 'type' => 'string' ), + 'requires_php' => array( 'type' => 'string' ), + 'update_available' => array( 'type' => 'boolean' ), + 'new_version' => array( 'type' => array( 'string', 'null' ) ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of plugins returned', 'airo-wp' ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'plugin-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $this->load_admin_file( 'plugin.php' ); + + $this->plugin_helper->refresh_updates( ! empty( $input['force_check'] ) ); + + $context = isset( $input['context'] ) ? sanitize_text_field( $input['context'] ) : 'view'; + $search = isset( $input['search'] ) ? sanitize_text_field( $input['search'] ) : ''; + $status = isset( $input['status'] ) ? sanitize_text_field( $input['status'] ) : ''; + + if ( ! in_array( $context, array( 'view', 'embed', 'edit' ), true ) ) { + $context = 'view'; + } + + $all_plugins = get_plugins(); + $results = array(); + + foreach ( $all_plugins as $file => $data ) { + $active = is_plugin_active( $file ); + + // Filter by status. + if ( 'active' === $status && ! $active ) { + continue; + } + if ( 'inactive' === $status && $active ) { + continue; + } + + // Filter by search term. + if ( ! empty( $search ) ) { + $name = isset( $data['Name'] ) ? $data['Name'] : ''; + $desc = isset( $data['Description'] ) ? $data['Description'] : ''; + if ( stripos( $name, $search ) === false && stripos( $desc, $search ) === false ) { + continue; + } + } + + // Derive slug from file path. + $slug = $this->get_slug_from_file( $file ); + + if ( 'embed' === $context ) { + $results[] = array( + 'slug' => $slug, + 'name' => isset( $data['Name'] ) ? $data['Name'] : '', + 'version' => isset( $data['Version'] ) ? $data['Version'] : '', + 'status' => $active ? 'active' : 'inactive', + ); + } else { + $update_info = $this->plugin_helper->get_update_info( $file ); + $results[] = array( + 'slug' => $slug, + 'name' => isset( $data['Name'] ) ? $data['Name'] : '', + 'version' => isset( $data['Version'] ) ? $data['Version'] : '', + 'author' => isset( $data['Author'] ) ? $data['Author'] : '', + 'description' => isset( $data['Description'] ) ? $data['Description'] : '', + 'status' => $active ? 'active' : 'inactive', + 'file' => $file, + 'plugin_uri' => isset( $data['PluginURI'] ) ? $data['PluginURI'] : '', + 'author_uri' => isset( $data['AuthorURI'] ) ? $data['AuthorURI'] : '', + 'text_domain' => isset( $data['TextDomain'] ) ? $data['TextDomain'] : '', + 'network_only' => ! empty( $data['Network'] ), + 'requires_wp' => isset( $data['RequiresWP'] ) ? $data['RequiresWP'] : '', + 'requires_php' => isset( $data['RequiresPHP'] ) ? $data['RequiresPHP'] : '', + 'update_available' => $update_info['update_available'], + 'new_version' => $update_info['new_version'], + ); + } + } + + return array( + 'success' => true, + 'plugins' => $results, + 'total' => count( $results ), + ); + } + + /** + * Derive a plugin slug from its file path. + * + * @param string $file Plugin file relative path. + * @return string Plugin slug. + */ + private function get_slug_from_file( string $file ): string { + $parts = explode( '/', $file ); + + return $parts[0]; + } +} diff --git a/includes/Mcp/Tools/Plugins/UpdatePlugin.php b/includes/Mcp/Tools/Plugins/UpdatePlugin.php new file mode 100644 index 0000000..8204f70 --- /dev/null +++ b/includes/Mcp/Tools/Plugins/UpdatePlugin.php @@ -0,0 +1,360 @@ +plugin_helper = $plugin_helper; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'activate_plugins' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Update Plugin', 'airo-wp' ), + 'description' => __( 'Updates an installed WordPress plugin from the WordPress.org repository. If "version" is provided, installs that exact version (allows upgrade or downgrade); otherwise updates to the latest available version.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'plugin-management', + ) + ); + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'plugin_slug' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug (e.g., "hello-dolly", "akismet")', 'airo-wp' ), + 'minLength' => 1, + ), + 'version' => array( + 'type' => 'string', + 'description' => __( 'Optional target version (e.g., "1.7.2"). If omitted, updates to the latest available version. Supplying a version supports both upgrade and downgrade.', 'airo-wp' ), + 'minLength' => 1, + ), + 'force_check' => array( + 'type' => 'boolean', + 'description' => __( 'When "version" is omitted, force a refresh of available plugin updates from WordPress.org before resolving the latest version. Ignored when "version" is supplied. Defaults to true so that "update to latest" reflects the freshest data available.', 'airo-wp' ), + 'default' => true, + ), + ), + 'required' => array( 'plugin_slug' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + public function get_output_schema(): array { + return $this->build_output_schema( + __( 'Plugin update result', 'airo-wp' ), + array( + 'plugin' => array( + 'type' => 'string', + 'description' => __( 'The plugin slug that was processed', 'airo-wp' ), + ), + 'previous_version' => array( + 'type' => 'string', + 'description' => __( 'The plugin version before the update', 'airo-wp' ), + ), + 'version' => array( + 'type' => 'string', + 'description' => __( 'The plugin version after the update', 'airo-wp' ), + ), + ) + ); + } + + /** + * Execute the update plugin tool. + * + * @param array $input Input parameters. + * @return array Plugin update result. + */ + public function execute( array $input ): array { + if ( empty( $input['plugin_slug'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Plugin slug is required', 'airo-wp' ), + 'plugin' => '', + ); + } + + $plugin_slug = sanitize_text_field( $input['plugin_slug'] ); + $target_version = ! empty( $input['version'] ) ? sanitize_text_field( $input['version'] ) : null; + + // Load wp-admin/includes/plugin.php unconditionally: this tool calls + // is_plugin_active(), deactivate_plugins(), activate_plugin(), and get_plugins(), + // all of which live in that file. load_admin_file() wraps require_once so the + // call is idempotent and cheap. + $this->load_admin_file( 'plugin.php' ); + + if ( ! function_exists( 'plugins_api' ) ) { + $this->load_admin_file( 'plugin-install.php' ); + } + + if ( ! function_exists( 'request_filesystem_credentials' ) ) { + $this->load_admin_file( 'file.php' ); + } + + if ( ! class_exists( 'Plugin_Upgrader' ) ) { + $this->load_admin_file( 'class-wp-upgrader.php' ); + } + + $plugin_file = $this->plugin_helper->find_file( $plugin_slug ); + if ( ! $plugin_file ) { + return array( + 'success' => false, + /* translators: %s: Plugin slug. */ + 'message' => sprintf( __( 'Plugin "%s" is not installed', 'airo-wp' ), $plugin_slug ), + 'plugin' => $plugin_slug, + ); + } + + $previous_version = $this->get_plugin_version( $plugin_file ); + + // Resolve the target version up front. When not supplied, look up the + // latest available version from the update_plugins transient. Unlike + // list-plugins / get-plugin, force_check defaults to true here: this + // is a write tool whose semantics are "install the latest version + // right now," so a stale transient could install a version that has + // already been superseded. Callers that want the cached-only path + // can pass force_check=false explicitly. + if ( null === $target_version ) { + $force_check = ! isset( $input['force_check'] ) || ! empty( $input['force_check'] ); + $this->plugin_helper->refresh_updates( $force_check ); + + $update_info = $this->plugin_helper->get_update_info( $plugin_file ); + if ( ! $update_info['update_available'] || empty( $update_info['new_version'] ) ) { + return array( + 'success' => true, + 'message' => __( 'Plugin is already up to date', 'airo-wp' ), + 'plugin' => $plugin_slug, + 'previous_version' => $previous_version, + 'version' => $previous_version, + ); + } + + $target_version = $update_info['new_version']; + } + + return $this->perform_update( $plugin_slug, $plugin_file, $previous_version, $target_version ); + } + + /** + * Install a specific version of a plugin from the WordPress.org repository. + * + * Supports both upgrade and downgrade by overwriting the existing install + * with the version-pinned package from the .org repo. Preserves activation + * state across the swap — if the install fails after deactivation, the + * plugin is reactivated before the error response is returned so it is + * never silently left disabled. + * + * @param string $plugin_slug Plugin slug. + * @param string $plugin_file Plugin file path relative to WP_PLUGIN_DIR. + * @param string $previous_version Plugin version before the update. + * @param string $target_version Concrete target version to install. + * @return array + */ + private function perform_update( string $plugin_slug, string $plugin_file, string $previous_version, string $target_version ): array { + if ( $target_version === $previous_version ) { + return array( + 'success' => true, + /* translators: %s: Plugin version number. */ + 'message' => sprintf( __( 'Plugin is already at version %s', 'airo-wp' ), $target_version ), + 'plugin' => $plugin_slug, + 'previous_version' => $previous_version, + 'version' => $previous_version, + ); + } + + $api = plugins_api( + 'plugin_information', + array( + 'slug' => $plugin_slug, + 'fields' => array( 'versions' => true ), + ) + ); + + if ( is_wp_error( $api ) ) { + return array( + 'success' => false, + /* translators: 1: Plugin slug, 2: Error message. */ + 'message' => sprintf( __( 'Plugin "%1$s" not found on WordPress.org: %2$s', 'airo-wp' ), $plugin_slug, $api->get_error_message() ), + 'plugin' => $plugin_slug, + 'previous_version' => $previous_version, + ); + } + + if ( empty( $api->versions ) || ! isset( $api->versions[ $target_version ] ) ) { + return array( + 'success' => false, + /* translators: 1: Version number, 2: Plugin slug. */ + 'message' => sprintf( __( 'Version "%1$s" is not available on WordPress.org for plugin "%2$s"', 'airo-wp' ), $target_version, $plugin_slug ), + 'plugin' => $plugin_slug, + 'previous_version' => $previous_version, + ); + } + + $download_url = $api->versions[ $target_version ]; + $was_active = is_plugin_active( $plugin_file ); + + // Deactivate before swapping files to avoid running mid-upgrade code. + if ( $was_active ) { + deactivate_plugins( $plugin_file, true ); + } + + $upgrader = new \Plugin_Upgrader( new \WP_Ajax_Upgrader_Skin() ); + $result = $upgrader->install( $download_url, array( 'overwrite_package' => true ) ); + + if ( is_wp_error( $result ) ) { + return $this->install_failure_response( + /* translators: 1: Version number, 2: Error message. */ + sprintf( __( 'Failed to install plugin version %1$s: %2$s', 'airo-wp' ), $target_version, $result->get_error_message() ), + $plugin_slug, + $plugin_file, + $previous_version, + $was_active + ); + } + + if ( ! $result ) { + return $this->install_failure_response( + /* translators: %s: Plugin version number. */ + sprintf( __( 'Plugin install of version %s failed for unknown reason', 'airo-wp' ), $target_version ), + $plugin_slug, + $plugin_file, + $previous_version, + $was_active + ); + } + + // Reactivate if it was active beforehand. Activation failure is reported + // in the message but does not flip success — the install itself worked. + $activation_message = ''; + if ( $was_active ) { + $activation_result = activate_plugin( $plugin_file ); + if ( is_wp_error( $activation_result ) ) { + /* translators: %s: Error message. */ + $activation_message = sprintf( __( ' Plugin installed but failed to reactivate: %s', 'airo-wp' ), $activation_result->get_error_message() ); + } + } + + $new_version = $this->get_plugin_version( $plugin_file ); + + return array( + 'success' => true, + /* translators: %s: Plugin version number. */ + 'message' => sprintf( __( 'Plugin updated to version %s', 'airo-wp' ), $new_version ) . $activation_message, + 'plugin' => $plugin_slug, + 'previous_version' => $previous_version, + 'version' => $new_version, + ); + } + + /** + * Build a failure response after an install attempt, restoring activation + * state first so a previously-active plugin is never silently left disabled + * after a failed install. + * + * @param string $message Failure message for the response. + * @param string $plugin_slug Plugin slug. + * @param string $plugin_file Plugin file path relative to WP_PLUGIN_DIR. + * @param string $previous_version Plugin version before the update. + * @param bool $was_active Whether the plugin was active before the deactivate call. + * @return array + */ + private function install_failure_response( string $message, string $plugin_slug, string $plugin_file, string $previous_version, bool $was_active ): array { + if ( $was_active ) { + $activation_result = activate_plugin( $plugin_file ); + if ( is_wp_error( $activation_result ) ) { + /* translators: %s: Error message. */ + $message .= sprintf( __( ' Additionally, the plugin could not be reactivated and is now disabled: %s', 'airo-wp' ), $activation_result->get_error_message() ); + } + } + + return array( + 'success' => false, + 'message' => $message, + 'plugin' => $plugin_slug, + 'previous_version' => $previous_version, + ); + } + + /** + * Get plugin version. + * + * @param string $plugin_file Plugin file path. + * @return string Plugin version. + */ + private function get_plugin_version( string $plugin_file ): string { + if ( ! function_exists( 'get_plugin_data' ) ) { + $this->load_admin_file( 'plugin.php' ); + } + + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false ); + return isset( $plugin_data['Version'] ) ? $plugin_data['Version'] : ''; + } +} diff --git a/includes/Mcp/Tools/Posts/CreatePost.php b/includes/Mcp/Tools/Posts/CreatePost.php new file mode 100644 index 0000000..f89f67d --- /dev/null +++ b/includes/Mcp/Tools/Posts/CreatePost.php @@ -0,0 +1,275 @@ + __( 'Create Post', 'airo-wp' ), + 'description' => __( 'Creates a new WordPress post, page, or custom post type', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Creation result. + */ + public function execute( array $input ): array { + if ( empty( $input['title'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Title is required', 'airo-wp' ), + ); + } + + $post_type = isset( $input['post_type'] ) && '' !== $input['post_type'] ? sanitize_key( $input['post_type'] ) : 'post'; + if ( ! post_type_exists( $post_type ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: post type slug */ + __( 'Post type %s does not exist', 'airo-wp' ), + $post_type + ), + ); + } + + if ( ! current_user_can( 'publish_posts' ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to create posts', 'airo-wp' ), + ); + } + + $post_data = array( + 'post_type' => $post_type, + 'post_title' => sanitize_text_field( $input['title'] ), + // All MCP callers are admins who have unfiltered_html; wp_insert_post() skips kses for them. + 'post_content' => isset( $input['content'] ) ? wp_kses_post( $input['content'] ) : '', + 'post_excerpt' => isset( $input['excerpt'] ) ? sanitize_textarea_field( $input['excerpt'] ) : '', + 'post_status' => isset( $input['status'] ) ? $input['status'] : 'draft', + ); + + if ( isset( $input['slug'] ) && '' !== $input['slug'] ) { + $sanitized_slug = sanitize_title( $input['slug'] ); + if ( '' === $sanitized_slug ) { + return array( + 'success' => false, + 'message' => __( 'Invalid slug provided. The slug cannot be empty after sanitization.', 'airo-wp' ), + ); + } + $post_data['post_name'] = $sanitized_slug; + } + + if ( isset( $input['status'] ) && ! empty( $input['status'] ) ) { + $allowed_statuses = array( 'publish', 'draft', 'private', 'pending', 'future' ); + if ( ! in_array( $input['status'], $allowed_statuses, true ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: post status */ + __( 'Invalid post status: %s', 'airo-wp' ), + $input['status'] + ), + ); + } + } + + if ( isset( $input['status'] ) && 'future' === $input['status'] && empty( $input['date'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Date is required when scheduling a post (status=future)', 'airo-wp' ), + ); + } + + if ( ! empty( $input['date'] ) ) { + $scheduled_date_raw = sanitize_text_field( $input['date'] ); + $scheduled_timestamp = strtotime( $scheduled_date_raw ); + + if ( false === $scheduled_timestamp ) { + return array( + 'success' => false, + 'message' => __( 'Invalid date format provided for scheduling', 'airo-wp' ), + ); + } + + if ( isset( $input['status'] ) && 'future' === $input['status'] && $scheduled_timestamp <= time() ) { + return array( + 'success' => false, + 'message' => __( 'Scheduled date must be in the future', 'airo-wp' ), + ); + } + + $post_data['post_date'] = $scheduled_date_raw; + } + + $post_id = wp_insert_post( $post_data, true ); + + if ( is_wp_error( $post_id ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Failed to create post: %s', 'airo-wp' ), + $post_id->get_error_message() + ), + ); + } + + wp_save_post_revision( $post_id ); + + $media_id = (int) ( $input['featured_media'] ?? 0 ); + if ( $media_id > 0 ) { + set_post_thumbnail( $post_id, $media_id ); + } + + if ( ! empty( $input['meta'] ) && is_array( $input['meta'] ) ) { + foreach ( $input['meta'] as $meta_item ) { + if ( ! is_array( $meta_item ) || empty( $meta_item['key'] ) ) { + continue; + } + + $meta_key = sanitize_text_field( $meta_item['key'] ); + if ( empty( $meta_key ) ) { + continue; + } + + $meta_value = isset( $meta_item['value'] ) ? $meta_item['value'] : ''; + if ( is_array( $meta_value ) ) { + $meta_value = array_map( 'sanitize_text_field', $meta_value ); + } else { + $meta_value = sanitize_text_field( $meta_value ); + } + + update_post_meta( $post_id, $meta_key, $meta_value ); + } + } + + return array( + 'success' => true, + 'post_id' => $post_id, + 'message' => __( 'Post created successfully', 'airo-wp' ), + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_type' => array( + 'type' => 'string', + 'description' => __( 'The post type (post, page, or custom post type). Defaults to post.', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The title for the new post', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The slug (URL-safe name) for the new post', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The content for the new post', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The excerpt for the new post', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The post status (publish, draft, private, etc.). Use "future" with date to schedule a post.', 'airo-wp' ), + 'enum' => array( 'publish', 'draft', 'private', 'pending', 'future' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The date the post should be published, in the site\'s timezone. Format: YYYY-MM-DD HH:MM:SS. Required for scheduling (status=future).', 'airo-wp' ), + ), + 'featured_media' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the media attachment to use as featured image', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'array', + 'description' => __( 'Array of meta fields to set', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'type' => 'string', + 'description' => __( 'Meta key', 'airo-wp' ), + ), + 'value' => array( + 'type' => array( 'string', 'array' ), + 'description' => __( 'Meta value (string or array)', 'airo-wp' ), + ), + ), + 'required' => array( 'key' ), + ), + ), + ), + 'required' => array( 'title' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Post creation result', 'airo-wp' ), + array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The new post ID', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Posts/DeletePost.php b/includes/Mcp/Tools/Posts/DeletePost.php new file mode 100644 index 0000000..4592518 --- /dev/null +++ b/includes/Mcp/Tools/Posts/DeletePost.php @@ -0,0 +1,158 @@ + __( 'Delete Post', 'airo-wp' ), + 'description' => __( 'Deletes a WordPress post or page by its ID. Can move to trash or permanently delete.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Deletion result. + */ + public function execute( array $input ): array { + if ( empty( $input['post_id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Post ID is required', 'airo-wp' ), + ); + } + + $post_id = (int) $input['post_id']; + $force_delete = isset( $input['force_delete'] ) ? (bool) $input['force_delete'] : true; + + $existing_post = get_post( $post_id ); + if ( ! $existing_post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d not found', 'airo-wp' ), + $post_id + ), + ); + } + + if ( ! $force_delete && 'trash' === $existing_post->post_status ) { + return array( + 'success' => false, + 'message' => __( 'Post is already in trash. Use force_delete to permanently delete it.', 'airo-wp' ), + ); + } + + if ( ! current_user_can( 'delete_post', $post_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to delete this post', 'airo-wp' ), + ); + } + + $result = wp_delete_post( $post_id, $force_delete ); + + if ( false === $result ) { + return array( + 'success' => false, + 'message' => __( 'Failed to delete post. The post may not exist or there was an error.', 'airo-wp' ), + ); + } + + $message = $force_delete + ? __( 'Post permanently deleted successfully', 'airo-wp' ) + : __( 'Post moved to trash successfully', 'airo-wp' ); + + return array( + 'success' => true, + 'post_id' => $post_id, + 'message' => $message, + 'deleted_permanently' => $force_delete, + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the post to delete', 'airo-wp' ), + 'minimum' => 1, + ), + 'force_delete' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to permanently delete the post (true) or move it to trash (false). Defaults to true.', 'airo-wp' ), + 'default' => true, + ), + ), + 'required' => array( 'post_id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Post deletion result', 'airo-wp' ), + array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The deleted post ID', 'airo-wp' ), + ), + 'deleted_permanently' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the post was permanently deleted (true) or moved to trash (false)', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Posts/GetPost.php b/includes/Mcp/Tools/Posts/GetPost.php new file mode 100644 index 0000000..7a80ab4 --- /dev/null +++ b/includes/Mcp/Tools/Posts/GetPost.php @@ -0,0 +1,185 @@ + __( 'Get Post', 'airo-wp' ), + 'description' => __( 'Retrieves a WordPress post by its ID', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Post data or not-found result. + */ + public function execute( array $input ): array { + $post_id = isset( $input['post_id'] ) ? (int) $input['post_id'] : 1; + $post = get_post( $post_id ); + + if ( ! $post ) { + return array( + 'id' => $post_id, + 'title' => 'Post not found', + 'content' => '', + 'excerpt' => '', + 'status' => 'not_found', + 'post_type' => 'post', + 'author_id' => 0, + 'date_created' => '', + 'date_modified' => '', + 'slug' => '', + 'featured_media_id' => 0, + 'meta' => array(), + ); + } + + $result = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, + 'status' => $post->post_status, + 'post_type' => $post->post_type, + 'author_id' => (int) $post->post_author, + 'date_created' => $post->post_date, + 'date_modified' => $post->post_modified, + 'slug' => $post->post_name, + 'featured_media_id' => (int) get_post_thumbnail_id( $post->ID ), + 'meta' => array(), + ); + + if ( ! empty( $input['include_meta'] ) ) { + $result['meta'] = get_post_meta( $post_id ); + } + + return $result; + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the post to retrieve', 'airo-wp' ), + 'minimum' => 1, + ), + 'include_meta' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include post meta data', 'airo-wp' ), + 'default' => true, + ), + ), + 'required' => array( 'post_id' ), + ); + } + + /** + * Get output schema for the tool. + * + * This method is public so that GetPostByOptionName can reuse the same schema. + * + * @return array + */ + public function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The post ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The post title', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The post content', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The post excerpt', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The post status (publish, draft, etc.)', 'airo-wp' ), + ), + 'post_type' => array( + 'type' => 'string', + 'description' => __( 'The post type', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The post author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The post creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The post modification date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The post slug', 'airo-wp' ), + ), + 'featured_media_id' => array( + 'type' => 'integer', + 'description' => __( 'The featured image attachment ID (0 if none)', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'object', + 'description' => __( 'Post meta data (if requested)', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Posts/GetPostByOptionName.php b/includes/Mcp/Tools/Posts/GetPostByOptionName.php new file mode 100644 index 0000000..703e29e --- /dev/null +++ b/includes/Mcp/Tools/Posts/GetPostByOptionName.php @@ -0,0 +1,118 @@ +get_post = $get_post; + } + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'manage_options' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Get post by option name', 'airo-wp' ), + 'description' => __( 'Reads a site option that stores a post ID, then returns that post.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_post->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Post data or not-found result. + */ + public function execute( array $input ): array { + $option_name = isset( $input['option_name'] ) ? sanitize_key( (string) $input['option_name'] ) : ''; + if ( '' === $option_name ) { + return $this->get_post->execute( array( 'post_id' => 0 ) ); + } + + $raw = get_option( $option_name, null ); + $post_id = 0; + if ( null !== $raw && '' !== $raw ) { + $n = is_numeric( $raw ) ? (int) $raw : 0; + $post_id = $n > 0 ? $n : 0; + } + + return $this->get_post->execute( + array( + 'post_id' => $post_id, + 'include_meta' => ! empty( $input['include_meta'] ), + ) + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'option_name' => array( + 'type' => 'string', + 'description' => __( 'Option name whose value is a post ID', 'airo-wp' ), + ), + 'include_meta' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include post meta.', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'option_name' ), + ); + } +} diff --git a/includes/Mcp/Tools/Posts/ListPostRevisions.php b/includes/Mcp/Tools/Posts/ListPostRevisions.php new file mode 100644 index 0000000..09fe8b6 --- /dev/null +++ b/includes/Mcp/Tools/Posts/ListPostRevisions.php @@ -0,0 +1,373 @@ + __( 'List Post Revisions', 'airo-wp' ), + 'description' => __( 'Retrieves a list of revisions for a post of any type (page, post, wp_navigation, etc.) with filtering and pagination options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array List of revisions or error. + */ + public function execute( array $input ): array { + if ( empty( $input['parent'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Parent post ID is required', 'airo-wp' ), + ); + } + + $parent_id = (int) $input['parent']; + + // Check if parent post exists — any post type is valid. + $parent_post = get_post( $parent_id ); + if ( ! $parent_post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d not found', 'airo-wp' ), + $parent_id + ), + ); + } + + // Build WP_Query args so pagination and filtering happen at the DB layer. + // wp_get_post_revisions() returns an already-sliced result, which makes + // accurate total / total_pages impossible without a second query, and it + // does not support search or include/exclude arguments. + $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 10; + $page = isset( $input['page'] ) ? (int) $input['page'] : 1; + $use_offset = isset( $input['offset'] ); + $offset = $use_offset ? (int) $input['offset'] : 0; + $has_search = ! empty( $input['search'] ); + + $args = array( + 'post_type' => 'revision', + 'post_parent' => $parent_id, + 'post_status' => 'inherit', + 'posts_per_page' => $per_page, + 'orderby' => $this->map_orderby( isset( $input['orderby'] ) ? $input['orderby'] : 'date', $has_search ), + 'order' => isset( $input['order'] ) ? strtoupper( $input['order'] ) : 'DESC', + ); + + // `offset` and `paged` are mutually exclusive in WP_Query — combining them + // silently breaks found_posts / max_num_pages. Honor offset exclusively + // when provided, otherwise use page-based pagination. + if ( $use_offset ) { + $args['offset'] = $offset; + } else { + $args['paged'] = $page; + } + + if ( $has_search ) { + $args['s'] = sanitize_text_field( $input['search'] ); + } + if ( ! empty( $input['include'] ) && is_array( $input['include'] ) ) { + $args['post__in'] = array_map( 'intval', $input['include'] ); + } + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) { + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- caller-supplied filter on bounded revision sets. + $args['post__not_in'] = array_map( 'intval', $input['exclude'] ); + } + + $query = new \WP_Query( $args ); + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + + $revision_data = array(); + if ( $query->have_posts() ) { + while ( $query->have_posts() ) { + $query->the_post(); + $revision_data[] = $this->format_revision( get_post(), $context ); + } + wp_reset_postdata(); + } + + // Report the effective page so the response is internally consistent + // regardless of which pagination mode the caller used. + $effective_page = $use_offset + ? ( (int) floor( $offset / max( 1, $per_page ) ) + 1 ) + : $page; + + return array( + 'revisions' => $revision_data, + 'total' => (int) $query->found_posts, + 'total_pages' => (int) $query->max_num_pages, + 'page' => $effective_page, + 'per_page' => $per_page, + ); + } + + /** + * Translate the schema's public `orderby` enum to a WP_Query-native key. + * + * The schema mirrors the WP REST API revisions controller, whose enum + * includes values WP_Query does not accept verbatim (`id`, `slug`, + * `include`, `include_slugs`). Passing those through unchanged causes + * WP_Query to silently fall back to default ordering. `relevance` is + * only meaningful when a search term is supplied — drop it otherwise. + * + * @param string $orderby Raw orderby value from input. + * @param bool $has_search Whether the query carries a search term. + * @return string WP_Query-compatible orderby value. + */ + private function map_orderby( string $orderby, bool $has_search ): string { + $map = array( + 'date' => 'date', + 'id' => 'ID', + 'slug' => 'name', + 'title' => 'title', + 'include' => 'post__in', + 'include_slugs' => 'post_name__in', + 'relevance' => 'relevance', + ); + + $mapped = isset( $map[ $orderby ] ) ? $map[ $orderby ] : 'date'; + + if ( 'relevance' === $mapped && ! $has_search ) { + return 'date'; + } + + return $mapped; + } + + /** + * Format revision data based on context. + * + * @param \WP_Post $revision Revision post object. + * @param string $context Context (view, embed, edit). + * @return array Formatted revision data. + */ + private function format_revision( \WP_Post $revision, string $context ): array { + $data = array( + 'id' => $revision->ID, + 'author_id' => (int) $revision->post_author, + 'date_created' => $revision->post_date, + 'date_modified' => $revision->post_modified, + 'parent_id' => (int) $revision->post_parent, + 'slug' => $revision->post_name, + ); + + switch ( $context ) { + case 'embed': + $data['title'] = $revision->post_title; + $data['excerpt'] = wp_trim_words( $revision->post_excerpt ? $revision->post_excerpt : $revision->post_content, 55 ); + break; + + case 'edit': + $data['title'] = $revision->post_title; + $data['content'] = $revision->post_content; + $data['excerpt'] = $revision->post_excerpt; + break; + + case 'view': + default: + $data['title'] = $revision->post_title; + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + $data['content'] = apply_filters( 'the_content', $revision->post_content ); + $data['excerpt'] = $revision->post_excerpt; + break; + } + + return $data; + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the parent post (any post type) for the revisions', 'airo-wp' ), + 'minimum' => 1, + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to be returned in result set', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Limit results to those matching a string', 'airo-wp' ), + ), + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- schema definition, not a query call. + 'exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes specific revision IDs', 'airo-wp' ), + ), + 'include' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to specific revision IDs', 'airo-wp' ), + ), + 'offset' => array( + 'type' => 'integer', + 'description' => __( 'Offset the result set by a specific number of items. When provided, takes precedence over `page` — the two are mutually exclusive in WP_Query and combining them breaks pagination totals.', 'airo-wp' ), + 'minimum' => 0, + ), + 'order' => array( + 'type' => 'string', + 'description' => __( 'Order sort attribute ascending or descending', 'airo-wp' ), + 'enum' => array( 'asc', 'desc' ), + 'default' => 'desc', + ), + 'orderby' => array( + 'type' => 'string', + 'description' => __( 'Sort collection by object attribute', 'airo-wp' ), + 'enum' => array( 'date', 'id', 'include', 'relevance', 'slug', 'include_slugs', 'title' ), + 'default' => 'date', + ), + ), + 'required' => array( 'parent' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + public function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'revisions' => array( + 'type' => 'array', + 'description' => __( 'Array of revision objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The revision ID', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The revision author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The revision creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The revision modification date', 'airo-wp' ), + ), + 'parent_id' => array( + 'type' => 'integer', + 'description' => __( 'The parent post ID', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The revision slug', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The revision title', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The revision content', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The revision excerpt', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of revisions', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page number', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of items per page', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Posts/ListPosts.php b/includes/Mcp/Tools/Posts/ListPosts.php new file mode 100644 index 0000000..f8c78b5 --- /dev/null +++ b/includes/Mcp/Tools/Posts/ListPosts.php @@ -0,0 +1,443 @@ + __( 'List Posts', 'airo-wp' ), + 'description' => __( 'Retrieves a list of WordPress posts, pages, or custom post types with filtering and pagination options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array List of posts with pagination info. + */ + public function execute( array $input ): array { + $query_args = $this->build_query_args( $input ); + + $query = new \WP_Query( $query_args ); + + $posts = array(); + $include_meta = ! empty( $input['include_meta'] ); + $fields_filter = ! empty( $input['_fields'] ) && is_array( $input['_fields'] ) ? $input['_fields'] : array(); + $context = isset( $input['context'] ) ? $input['context'] : 'view'; + $include_featured = empty( $fields_filter ) + || in_array( 'featured_media_id', array_map( 'sanitize_key', $fields_filter ), true ); + + if ( $query->have_posts() ) { + while ( $query->have_posts() ) { + $query->the_post(); + $post = get_post(); + + $complete_post_data = $this->build_post_data( $post, $context, $include_meta, $include_featured ); + + if ( ! empty( $fields_filter ) ) { + $post_data = $this->filter_fields( $complete_post_data, $fields_filter ); + } else { + $post_data = $complete_post_data; + } + + $posts[] = $post_data; + } + wp_reset_postdata(); + } + + $page = isset( $input['page'] ) ? (int) $input['page'] : 1; + $per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : 10; + $total = $query->found_posts; + $total_pages = max( 1, (int) ceil( $total / $per_page ) ); + + return array( + 'posts' => $posts, + 'total' => $total, + 'total_pages' => $total_pages, + 'page' => $page, + 'per_page' => $per_page, + ); + } + + /** + * Build WP_Query arguments from input parameters. + * + * @param array $input Input parameters. + * @return array WP_Query arguments. + */ + private function build_query_args( array $input ): array { + $post_type = isset( $input['post_type'] ) && '' !== $input['post_type'] ? sanitize_key( $input['post_type'] ) : 'page'; + + $args = array( + 'post_type' => $post_type, + 'post_status' => isset( $input['status'] ) ? $input['status'] : 'publish', + 'posts_per_page' => isset( $input['per_page'] ) ? (int) $input['per_page'] : 10, + 'paged' => isset( $input['page'] ) ? (int) $input['page'] : 1, + 'order' => isset( $input['order'] ) ? strtoupper( $input['order'] ) : 'DESC', + 'orderby' => isset( $input['orderby'] ) ? $input['orderby'] : 'date', + ); + + if ( ! empty( $input['search'] ) ) { + $args['s'] = sanitize_text_field( $input['search'] ); + } + + if ( isset( $input['author'] ) ) { + $args['author'] = (int) $input['author']; + } + + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) { + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- caller-supplied filter on bounded result sets. + $args['post__not_in'] = array_map( 'intval', $input['exclude'] ); + } + + if ( ! empty( $input['include'] ) && is_array( $input['include'] ) ) { + $args['post__in'] = array_map( 'intval', $input['include'] ); + if ( 'include' === $args['orderby'] ) { + $args['orderby'] = 'post__in'; + } + } + + if ( isset( $input['parent'] ) ) { + $args['post_parent'] = (int) $input['parent']; + } + + if ( ! empty( $input['parent_exclude'] ) && is_array( $input['parent_exclude'] ) ) { + $args['post_parent__not_in'] = array_map( 'intval', $input['parent_exclude'] ); + } + + if ( isset( $input['featured_media_id'] ) && (int) $input['featured_media_id'] > 0 ) { + $args['meta_query'] = isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ? $args['meta_query'] : array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $args['meta_query'][] = array( + 'key' => '_thumbnail_id', + 'value' => (int) $input['featured_media_id'], + 'compare' => '=', + 'type' => 'NUMERIC', + ); + } + + if ( ! empty( $input['slug'] ) ) { + if ( is_array( $input['slug'] ) ) { + $args['post_name__in'] = array_map( 'sanitize_title', $input['slug'] ); + } else { + $args['post_name__in'] = array( sanitize_title( $input['slug'] ) ); + } + } + + return $args; + } + + /** + * Build post data based on context. + * + * @param \WP_Post $post Post object. + * @param string $context Context (view, embed, edit). + * @param bool $include_meta Whether to include meta data. + * @param bool $include_featured Whether to include featured_media_id. + * @return array Post data. + */ + private function build_post_data( \WP_Post $post, string $context, bool $include_meta, bool $include_featured = true ): array { + $post_data = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'slug' => $post->post_name, + 'status' => $post->post_status, + 'parent_id' => (int) $post->post_parent, + 'menu_order' => (int) $post->menu_order, + ); + + if ( $include_featured ) { + $post_data['featured_media_id'] = (int) get_post_thumbnail_id( $post->ID ); + } + + switch ( $context ) { + case 'embed': + $post_data['excerpt'] = wp_trim_words( $post->post_excerpt ? $post->post_excerpt : $post->post_content, 55 ); + $post_data['date_created'] = $post->post_date; + $post_data['date_modified'] = $post->post_modified; + break; + + case 'edit': + $post_data['content'] = $post->post_content; + $post_data['excerpt'] = $post->post_excerpt; + $post_data['author_id'] = (int) $post->post_author; + $post_data['date_created'] = $post->post_date; + $post_data['date_modified'] = $post->post_modified; + + if ( $include_meta ) { + $post_data['meta'] = get_post_meta( $post->ID ); + } + break; + + case 'view': + default: + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying core WP filter. + $post_data['content'] = apply_filters( 'the_content', $post->post_content ); + $post_data['excerpt'] = $post->post_excerpt; + $post_data['author_id'] = (int) $post->post_author; + $post_data['date_created'] = $post->post_date; + $post_data['date_modified'] = $post->post_modified; + + if ( $include_meta ) { + $post_data['meta'] = get_post_meta( $post->ID ); + } + break; + } + + return $post_data; + } + + /** + * Filter post data to only include specified fields. + * + * @param array $post_data Complete post data. + * @param array $fields Array of field names to include. + * @return array Filtered post data. + */ + private function filter_fields( array $post_data, array $fields ): array { + $filtered = array(); + + if ( isset( $post_data['id'] ) ) { + $filtered['id'] = $post_data['id']; + } + + foreach ( $fields as $field ) { + $field = sanitize_key( $field ); + if ( isset( $post_data[ $field ] ) && 'id' !== $field ) { + $filtered[ $field ] = $post_data[ $field ]; + } + } + + return $filtered; + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_type' => array( + 'type' => 'string', + 'description' => __( 'The post type (post, page, or custom post type). Defaults to page.', 'airo-wp' ), + 'default' => 'page', + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to be returned in result set', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Limit results to those matching a string', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Limit result set to posts assigned to specific author ID', 'airo-wp' ), + ), + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- schema definition, not a query call. + 'exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Ensure result set excludes specific IDs', 'airo-wp' ), + ), + 'include' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to specific IDs', 'airo-wp' ), + ), + 'order' => array( + 'type' => 'string', + 'description' => __( 'Order sort attribute ascending or descending', 'airo-wp' ), + 'enum' => array( 'asc', 'desc' ), + 'default' => 'desc', + ), + 'orderby' => array( + 'type' => 'string', + 'description' => __( 'Sort collection by post attribute', 'airo-wp' ), + 'enum' => array( 'author', 'date', 'id', 'include', 'modified', 'parent', 'title', 'menu_order' ), + 'default' => 'date', + ), + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'Limit result set to items with particular parent ID', 'airo-wp' ), + ), + 'parent_exclude' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'Limit result set to all items except those of a particular parent ID', 'airo-wp' ), + ), + 'slug' => array( + 'type' => array( 'string', 'array' ), + 'items' => array( 'type' => 'string' ), + 'description' => __( 'Limit result set to posts with one or more specific slugs. Can be a single slug string or an array of slugs', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Limit result set to posts assigned one or more statuses', 'airo-wp' ), + 'enum' => array( 'publish', 'future', 'draft', 'pending', 'private', 'trash', 'any' ), + 'default' => 'publish', + ), + 'featured_media_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'Limit result set to items whose featured image is the given attachment ID', 'airo-wp' ), + ), + 'include_meta' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include post meta data', 'airo-wp' ), + 'default' => false, + ), + '_fields' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + 'description' => __( 'Limit response to specific fields. Available fields: id, title, content, excerpt, status, author_id, date_created, date_modified, slug, parent_id, menu_order, featured_media_id, meta', 'airo-wp' ), + ), + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'view', + ), + ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'posts' => array( + 'type' => 'array', + 'description' => __( 'Array of post objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The post ID', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The post title', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The post content', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The post excerpt', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The post status', 'airo-wp' ), + ), + 'author_id' => array( + 'type' => 'integer', + 'description' => __( 'The post author ID', 'airo-wp' ), + ), + 'date_created' => array( + 'type' => 'string', + 'description' => __( 'The post creation date', 'airo-wp' ), + ), + 'date_modified' => array( + 'type' => 'string', + 'description' => __( 'The post modification date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The post slug', 'airo-wp' ), + ), + 'parent_id' => array( + 'type' => 'integer', + 'description' => __( 'The parent post ID', 'airo-wp' ), + ), + 'menu_order' => array( + 'type' => 'integer', + 'description' => __( 'The post menu order', 'airo-wp' ), + ), + 'featured_media_id' => array( + 'type' => 'integer', + 'description' => __( 'The featured image attachment ID (0 if none)', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'object', + 'description' => __( 'Post meta data (if requested)', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of posts matching the query', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages available', 'airo-wp' ), + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page number', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of items per page', 'airo-wp' ), + ), + ), + ); + } +} diff --git a/includes/Mcp/Tools/Posts/RestorePostRevision.php b/includes/Mcp/Tools/Posts/RestorePostRevision.php new file mode 100644 index 0000000..872b56c --- /dev/null +++ b/includes/Mcp/Tools/Posts/RestorePostRevision.php @@ -0,0 +1,218 @@ + __( 'Restore Post Revision', 'airo-wp' ), + 'description' => __( 'Restores a post to a previous revision. Creates a new revision with the restored content — history is forward-only.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Restore result. + */ + public function execute( array $input ): array { + // Load revision functions. + $this->load_admin_file( 'post.php' ); + + // Validate required parameters. + if ( empty( $input['parent'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Parent post ID is required', 'airo-wp' ), + ); + } + + if ( empty( $input['id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Revision ID is required', 'airo-wp' ), + ); + } + + $parent_id = (int) $input['parent']; + $revision_id = (int) $input['id']; + + // Check if parent post exists. + $parent_post = get_post( $parent_id ); + if ( ! $parent_post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d not found', 'airo-wp' ), + $parent_id + ), + ); + } + + // Check if the post type supports revisions. + if ( ! post_type_supports( $parent_post->post_type, 'revisions' ) ) { + return array( + 'success' => false, + 'message' => __( 'This post type does not support revisions', 'airo-wp' ), + ); + } + + // Get the revision. + $revision = wp_get_post_revision( $revision_id ); + + if ( ! $revision ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: revision ID */ + __( 'Revision with ID %d not found', 'airo-wp' ), + $revision_id + ), + ); + } + + // Verify the revision belongs to the parent post. + if ( (int) $revision->post_parent !== $parent_id ) { + return array( + 'success' => false, + 'message' => __( 'Revision does not belong to the specified parent post', 'airo-wp' ), + ); + } + + // Check if user has permission to edit the post. + if ( ! current_user_can( 'edit_post', $parent_id ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to edit this post', 'airo-wp' ), + ); + } + + // Restore the revision — this creates a new revision with the restored content. + $restored_post_id = wp_restore_post_revision( $revision_id ); + + if ( ! $restored_post_id || is_wp_error( $restored_post_id ) ) { + $error_message = is_wp_error( $restored_post_id ) + ? $restored_post_id->get_error_message() + : __( 'Failed to restore revision', 'airo-wp' ); + return array( + 'success' => false, + 'message' => $error_message, + ); + } + + // Get the latest revision after restore to return the new revision ID. + $latest_revisions = wp_get_post_revisions( + $parent_id, + array( + 'numberposts' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + ) + ); + $current_revision_id = (int) array_key_first( $latest_revisions ); + + return array( + 'success' => true, + 'message' => sprintf( + /* translators: 1: post ID, 2: revision ID */ + __( 'Post %1$d restored to revision %2$d', 'airo-wp' ), + $parent_id, + $revision_id + ), + 'post_id' => $parent_id, + 'restored_revision_id' => $revision_id, + 'current_revision_id' => $current_revision_id, + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the parent post', 'airo-wp' ), + 'minimum' => 1, + ), + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the revision to restore', 'airo-wp' ), + 'minimum' => 1, + ), + ), + 'required' => array( 'parent', 'id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Post revision restore result', 'airo-wp' ), + array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The post ID that was restored', 'airo-wp' ), + ), + 'restored_revision_id' => array( + 'type' => 'integer', + 'description' => __( 'The revision ID that was restored from', 'airo-wp' ), + ), + 'current_revision_id' => array( + 'type' => 'integer', + 'description' => __( 'The new revision ID created by the restore', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Posts/UpdatePost.php b/includes/Mcp/Tools/Posts/UpdatePost.php new file mode 100644 index 0000000..f25ab28 --- /dev/null +++ b/includes/Mcp/Tools/Posts/UpdatePost.php @@ -0,0 +1,313 @@ + __( 'Update Post', 'airo-wp' ), + 'description' => __( 'Updates a WordPress post by its ID with new title, content, excerpt, status, and/or post meta fields', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Update result. + */ + public function execute( array $input ): array { + if ( empty( $input['post_id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Post ID is required', 'airo-wp' ), + ); + } + + $post_id = (int) $input['post_id']; + + $existing_post = get_post( $post_id ); + if ( ! $existing_post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d not found', 'airo-wp' ), + $post_id + ), + ); + } + + $update_data = array( + 'ID' => $post_id, + ); + + $updated_fields = array(); + + if ( isset( $input['title'] ) && ! empty( $input['title'] ) ) { + $update_data['post_title'] = sanitize_text_field( $input['title'] ); + $updated_fields[] = 'title'; + } + + if ( isset( $input['slug'] ) && '' !== $input['slug'] ) { + $sanitized_slug = sanitize_title( $input['slug'] ); + if ( '' === $sanitized_slug ) { + return array( + 'success' => false, + 'message' => __( 'Invalid slug provided. The slug cannot be empty after sanitization.', 'airo-wp' ), + ); + } + $update_data['post_name'] = $sanitized_slug; + $updated_fields[] = 'slug'; + } + + if ( isset( $input['content'] ) ) { + // All MCP callers are admins who have unfiltered_html; wp_update_post() skips kses for them. + $update_data['post_content'] = wp_kses_post( $input['content'] ); + $updated_fields[] = 'content'; + } + + if ( isset( $input['excerpt'] ) ) { + $update_data['post_excerpt'] = sanitize_textarea_field( $input['excerpt'] ); + $updated_fields[] = 'excerpt'; + } + + if ( isset( $input['status'] ) && ! empty( $input['status'] ) ) { + $allowed_statuses = array( 'publish', 'draft', 'private', 'pending', 'future' ); + if ( in_array( $input['status'], $allowed_statuses, true ) ) { + $update_data['post_status'] = $input['status']; + $updated_fields[] = 'status'; + } + } + + if ( isset( $input['status'] ) && 'future' === $input['status'] && empty( $input['date'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Date is required when scheduling a post (status=future)', 'airo-wp' ), + ); + } + + if ( ! empty( $input['date'] ) ) { + $scheduled_date_raw = sanitize_text_field( $input['date'] ); + $scheduled_timestamp = strtotime( $scheduled_date_raw ); + + if ( false === $scheduled_timestamp ) { + return array( + 'success' => false, + 'message' => __( 'Invalid date format provided for scheduling', 'airo-wp' ), + ); + } + + if ( isset( $input['status'] ) && 'future' === $input['status'] && $scheduled_timestamp <= time() ) { + return array( + 'success' => false, + 'message' => __( 'Scheduled date must be in the future', 'airo-wp' ), + ); + } + + $update_data['post_date'] = $scheduled_date_raw; + $updated_fields[] = 'date'; + } + + $has_update_data = ! empty( $updated_fields ) + || ( isset( $input['meta'] ) && is_array( $input['meta'] ) && ! empty( $input['meta'] ) ) + || ! empty( $input['featured_media'] ); + + if ( ! $has_update_data ) { + return array( + 'success' => true, + 'post_id' => $post_id, + 'message' => __( 'No updates requested', 'airo-wp' ), + 'updated_fields' => array(), + 'updated_meta' => array(), + ); + } + + $result = wp_update_post( $update_data, true ); + + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Failed to update post: %s', 'airo-wp' ), + $result->get_error_message() + ), + ); + } + + $updated_meta = array(); + + if ( ! empty( $input['meta'] ) && is_array( $input['meta'] ) ) { + foreach ( $input['meta'] as $meta_item ) { + if ( ! is_array( $meta_item ) || empty( $meta_item['key'] ) ) { + continue; + } + + $meta_key = sanitize_text_field( $meta_item['key'] ); + + if ( empty( $meta_key ) ) { + continue; + } + + $meta_value = isset( $meta_item['value'] ) ? $meta_item['value'] : ''; + + if ( is_array( $meta_value ) ) { + $meta_value = array_map( 'sanitize_text_field', $meta_value ); + } else { + $meta_value = sanitize_text_field( $meta_value ); + } + + $meta_result = update_post_meta( $post_id, $meta_key, $meta_value ); + + if ( false !== $meta_result ) { + $updated_meta[] = $meta_key; + } + } + } + + $media_id = (int) ( $input['featured_media'] ?? 0 ); + if ( $media_id > 0 ) { + set_post_thumbnail( $post_id, $media_id ); + $updated_fields[] = 'featured_media'; + } + + return array( + 'success' => true, + 'post_id' => $post_id, + 'message' => __( 'Post updated successfully', 'airo-wp' ), + 'updated_fields' => $updated_fields, + 'updated_meta' => $updated_meta, + ); + } + + /** + * Get input schema. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the post to update', 'airo-wp' ), + 'minimum' => 1, + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'The new title for the post', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The slug (URL-safe name) for the post', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The new content for the post', 'airo-wp' ), + ), + 'excerpt' => array( + 'type' => 'string', + 'description' => __( 'The new excerpt for the post', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The post status (publish, draft, private, etc.). Use "future" with date to schedule a post.', 'airo-wp' ), + 'enum' => array( 'publish', 'draft', 'private', 'pending', 'future' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'The date the post should be published, in the site\'s timezone. Format: YYYY-MM-DD HH:MM:SS. Required for scheduling (status=future).', 'airo-wp' ), + ), + 'meta' => array( + 'type' => 'array', + 'description' => __( 'Array of meta fields to update', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'type' => 'string', + 'description' => __( 'Meta key', 'airo-wp' ), + ), + 'value' => array( + 'type' => array( 'string', 'array' ), + 'description' => __( 'Meta value (string or array)', 'airo-wp' ), + ), + ), + 'required' => array( 'key' ), + ), + ), + 'featured_media' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the media attachment to use as featured image', 'airo-wp' ), + ), + ), + 'required' => array( 'post_id' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Post update result', 'airo-wp' ), + array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The updated post ID', 'airo-wp' ), + ), + 'updated_fields' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + 'description' => __( 'List of fields that were updated', 'airo-wp' ), + ), + 'updated_meta' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + 'description' => __( 'List of meta keys that were updated', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Posts/UpdatePostImageAltText.php b/includes/Mcp/Tools/Posts/UpdatePostImageAltText.php new file mode 100644 index 0000000..c8378b0 --- /dev/null +++ b/includes/Mcp/Tools/Posts/UpdatePostImageAltText.php @@ -0,0 +1,416 @@ + __( 'Update Post Image Alt Text', 'airo-wp' ), + 'description' => __( 'Updates the alternative text for a specific image block within a WordPress post', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'content-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Update result or error. + */ + public function execute( array $input ): array { + // Validate required parameters. + if ( empty( $input['post_id'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Post ID is required', 'airo-wp' ), + ); + } + + if ( empty( $input['image_src'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Image source URL is required', 'airo-wp' ), + ); + } + + if ( ! isset( $input['alt'] ) || '' === trim( $input['alt'] ) ) { + return array( + 'success' => false, + 'message' => __( 'Alt text is required', 'airo-wp' ), + ); + } + + $post_id = (int) $input['post_id']; + $image_src = esc_url_raw( trim( $input['image_src'] ) ); + $alt = trim( $input['alt'] ); + + // Check if post exists. + $post = get_post( $post_id ); + if ( ! $post ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %d: post ID */ + __( 'Post with ID %d not found', 'airo-wp' ), + $post_id + ), + ); + } + + // Parse the post content into blocks. + $blocks = parse_blocks( $post->post_content ); + $normalized_image_src = $this->normalize_url( $image_src ); + + // Process all blocks to find and update the image. + $updated = $this->find_and_update_image_block( $blocks, $image_src, $normalized_image_src, $alt ); + + // Check if the image block was found. + if ( ! $updated ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: 1: image source URL, 2: post ID */ + __( 'Image with source URL "%1$s" not found in post %2$d', 'airo-wp' ), + $image_src, + $post_id + ), + ); + } + + // Update the post with modified content. + $result = wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + // Check for errors. + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Failed to update post: %s', 'airo-wp' ), + $result->get_error_message() + ), + ); + } + + return array( + 'success' => true, + 'post_id' => $post_id, + 'image_src' => $image_src, + 'message' => __( 'Image alt text updated successfully', 'airo-wp' ), + ); + } + + /** + * Find and update image block with matching URL (recursive). + * + * Recursively searches through blocks to find an image block with a URL + * matching the provided image source, then updates its alt text. + * + * @param array> &$blocks The blocks array to search (passed by reference). + * @param string $image_src The original image source URL to match. + * @param string $normalized_image_src The normalized image source URL. + * @param string $alt The new alt text to set. + * @return bool True if a matching image block was found and updated, false otherwise. + */ + private function find_and_update_image_block( array &$blocks, string $image_src, string $normalized_image_src, string $alt ): bool { + foreach ( $blocks as &$block ) { + $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; + + // Skip non-image blocks but process their inner blocks. + if ( 'core/image' !== $block_name ) { + if ( ! empty( $block['innerBlocks'] ) ) { + if ( $this->find_and_update_image_block( $block['innerBlocks'], $image_src, $normalized_image_src, $alt ) ) { + return true; + } + } + continue; + } + + // Check block attrs URL (new block format). + if ( $this->check_and_update_block_attrs_url( $block, $image_src, $normalized_image_src, $alt ) ) { + return true; + } + + // Check innerHTML src attribute (classic block format). + if ( $this->check_and_update_html_src( $block, $image_src, $normalized_image_src, $alt ) ) { + return true; + } + + // Check srcset attribute (responsive images). + if ( $this->check_and_update_srcset( $block, $image_src, $normalized_image_src, $alt ) ) { + return true; + } + + // Recursively process inner blocks. + if ( ! empty( $block['innerBlocks'] ) ) { + if ( $this->find_and_update_image_block( $block['innerBlocks'], $image_src, $normalized_image_src, $alt ) ) { + return true; + } + } + } + + return false; + } + + /** + * Check and update block if attrs URL matches. + * + * Checks if the block's attrs contain a URL that matches the target image + * source. This handles the new block editor format where image URLs are + * stored in block attributes. + * + * @param array &$block The block to check (passed by reference). + * @param string $image_src The original image source URL to match. + * @param string $normalized_image_src The normalized image source URL. + * @param string $alt The new alt text to set. + * @return bool True if the URL matched and block was updated, false otherwise. + */ + private function check_and_update_block_attrs_url( array &$block, string $image_src, string $normalized_image_src, string $alt ): bool { + if ( empty( $block['attrs']['url'] ) ) { + return false; + } + + $block_url = $block['attrs']['url']; + $normalized_block_url = $this->normalize_url( $block_url ); + + if ( $block_url === $image_src || $normalized_block_url === $normalized_image_src ) { + $this->update_block_alt_text( $block, $alt ); + return true; + } + + return false; + } + + /** + * Check and update block if innerHTML src attribute matches. + * + * Extracts the src attribute from the block's innerHTML and checks if it + * matches the target image source. This handles the classic block format + * where the image is stored as raw HTML. + * + * @param array &$block The block to check (passed by reference). + * @param string $image_src The original image source URL to match. + * @param string $normalized_image_src The normalized image source URL. + * @param string $alt The new alt text to set. + * @return bool True if the src matched and block was updated, false otherwise. + */ + private function check_and_update_html_src( array &$block, string $image_src, string $normalized_image_src, string $alt ): bool { + if ( empty( $block['innerHTML'] ) ) { + return false; + } + + // Extract src from img tag. + if ( ! preg_match( '/src="([^"]+)"/', $block['innerHTML'], $matches ) ) { + return false; + } + + $html_src = $matches[1]; + $normalized_html_src = $this->normalize_url( $html_src ); + + if ( $html_src === $image_src || $normalized_html_src === $normalized_image_src ) { + $this->update_block_alt_text( $block, $alt ); + return true; + } + + return false; + } + + /** + * Check and update block if srcset attribute contains matching URL. + * + * Checks if the block's innerHTML contains a srcset attribute that includes + * the target image source. The srcset attribute contains multiple image URLs + * for responsive images (format: "url1 size1, url2 size2, ..."). + * + * @param array &$block The block to check (passed by reference). + * @param string $image_src The original image source URL to match. + * @param string $normalized_image_src The normalized image source URL. + * @param string $alt The new alt text to set. + * @return bool True if the srcset contained the URL and block was updated, false otherwise. + */ + private function check_and_update_srcset( array &$block, string $image_src, string $normalized_image_src, string $alt ): bool { + if ( empty( $block['innerHTML'] ) ) { + return false; + } + + // Extract srcset from img tag. + if ( ! preg_match( '/srcset="([^"]+)"/', $block['innerHTML'], $matches ) ) { + return false; + } + + $srcset = $matches[1]; + + // Parse srcset into individual candidates ("url size, url size, ..."). + $candidates = array_map( 'trim', explode( ',', $srcset ) ); + + foreach ( $candidates as $candidate ) { + if ( '' === $candidate ) { + continue; + } + + // Split on whitespace to separate URL from any size descriptor (e.g., "300w", "2x"). + $parts = preg_split( '/\s+/', $candidate ); + if ( empty( $parts ) || empty( $parts[0] ) ) { + continue; + } + + $candidate_url = $parts[0]; + $normalized_candidate_url = $this->normalize_url( $candidate_url ); + + if ( $candidate_url === $image_src || $normalized_candidate_url === $normalized_image_src ) { + $this->update_block_alt_text( $block, $alt ); + return true; + } + } + + return false; + } + + /** + * Normalize URL for comparison by removing size suffixes. + * + * Removes size suffixes like -300x200, -150x150, etc., before any file + * extension. This allows matching images regardless of which size variant + * is being used. + * + * @param string $url The URL to normalize. + * @return string The normalized URL. + */ + private function normalize_url( string $url ): string { + // Remove query string and fragment so the file extension is at the end of the string. + $parts = preg_split( '/[?#]/', $url, 2 ); + $base_url = $parts[0]; + + return preg_replace( '/-\d+x\d+(?=\.[a-z0-9]+$)/i', '', $base_url ); + } + + /** + * Update the alt text in a block. + * + * @param array &$block The block to update (passed by reference). + * @param string $alt The new alt text. + */ + private function update_block_alt_text( array &$block, string $alt ): void { + // Update attrs alt (for block editor). + $block['attrs']['alt'] = $alt; + + // Update innerHTML alt attribute if present. + if ( ! empty( $block['innerHTML'] ) ) { + // Check if alt attribute already exists. + if ( false !== strpos( $block['innerHTML'], 'alt=' ) ) { + // Replace existing alt attribute. + $block['innerHTML'] = preg_replace( + '/(]+\s)alt="[^"]*"/', + '${1}alt="' . esc_attr( $alt ) . '"', + $block['innerHTML'] + ); + } else { + // Add alt attribute if it doesn't exist (after opening ' . esc_attr( $alt ) . ' + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the post containing the image', 'airo-wp' ), + 'minimum' => 1, + ), + 'image_src' => array( + 'type' => 'string', + 'description' => __( 'The source URL of the image to update', 'airo-wp' ), + ), + 'alt' => array( + 'type' => 'string', + 'description' => __( 'The new alternative text for the image', 'airo-wp' ), + ), + ), + 'required' => array( 'post_id', 'image_src', 'alt' ), + ); + } + + /** + * Get output schema. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Post image alt text update result', 'airo-wp' ), + array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the updated post', 'airo-wp' ), + ), + 'image_src' => array( + 'type' => 'string', + 'description' => __( 'The source URL of the image that was updated', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Site/SiteInfo.php b/includes/Mcp/Tools/Site/SiteInfo.php new file mode 100644 index 0000000..5ac7331 --- /dev/null +++ b/includes/Mcp/Tools/Site/SiteInfo.php @@ -0,0 +1,298 @@ + __( 'Get Site Information', 'airo-wp' ), + 'description' => __( 'Retrieves basic information about the current WordPress site', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'site-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Site information. + */ + public function execute( array $input ): array { + $site_logo = (int) get_option( 'site_logo', 0 ); + $site_icon = (int) get_option( 'site_icon', 0 ); + + $result = array( + 'site_name' => get_bloginfo( 'name' ), + 'site_url' => get_site_url(), + 'description' => get_bloginfo( 'description' ), + 'wordpress_version' => get_bloginfo( 'version' ), + 'is_published' => (bool) get_option( 'gdl_site_published', false ), + // get_locale() is the canonical accessor since WP 4.0; the WPLANG option + // is deprecated and often empty even on sites with a configured locale. + 'site_locale' => get_locale(), + // wp_timezone_string() (WP 5.3+) returns the named zone if set, else + // converts gmt_offset to "+HH:MM" form. Reading timezone_string alone + // returns "" for sites configured with a UTC offset instead of a city. + // The plugin requires WP 6.8+, so wp_timezone_string() is always available. + 'timezone' => wp_timezone_string(), + 'date_format' => get_option( 'date_format', 'F j, Y' ), + 'time_format' => get_option( 'time_format', 'g:i a' ), + 'posts_per_page' => (int) get_option( 'posts_per_page', 10 ), + 'blog_public' => (bool) get_option( 'blog_public', true ), + 'site_logo' => $site_logo > 0 ? $site_logo : null, + 'site_icon' => $site_icon > 0 ? $site_icon : null, + ); + + if ( ! empty( $input['include_stats'] ) ) { + $result['stats'] = array( + 'post_count' => $this->get_published_post_count( 'post' ), + 'page_count' => $this->get_published_post_count( 'page' ), + ); + } + + if ( ! empty( $input['include_theme_info'] ) ) { + $result['theme_info'] = $this->get_theme_info(); + } + + if ( ! empty( $input['include_plugin_count'] ) ) { + $result['plugin_count'] = $this->get_active_plugin_count(); + } + + if ( ! empty( $input['include_reading_settings'] ) ) { + $result['reading_settings'] = $this->get_reading_settings(); + } + + return $result; + } + + /** + * Input JSON Schema for this tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'include_stats' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include post/page statistics', 'airo-wp' ), + 'default' => false, + ), + 'include_theme_info' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include active theme information', 'airo-wp' ), + 'default' => false, + ), + 'include_plugin_count' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include plugin count information', 'airo-wp' ), + 'default' => false, + ), + 'include_reading_settings' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to include reading settings', 'airo-wp' ), + 'default' => false, + ), + ), + ); + } + + /** + * Output JSON Schema for this tool. + * + * @return array + */ + private function get_output_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'site_name' => array( + 'type' => 'string', + 'description' => __( 'Site name', 'airo-wp' ), + ), + 'site_url' => array( + 'type' => 'string', + 'description' => __( 'Site URL', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'Site tagline', 'airo-wp' ), + ), + 'wordpress_version' => array( + 'type' => 'string', + 'description' => __( 'WordPress version', 'airo-wp' ), + ), + 'is_published' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the site has been published', 'airo-wp' ), + ), + 'site_locale' => array( + 'type' => 'string', + 'description' => __( 'Active site locale from get_locale() (e.g. "es_ES", "en_US")', 'airo-wp' ), + ), + 'timezone' => array( + 'type' => 'string', + 'description' => __( 'Timezone — named zone (e.g. "America/New_York") or "+HH:MM"/"-HH:MM" offset for sites configured with a UTC offset', 'airo-wp' ), + ), + 'date_format' => array( + 'type' => 'string', + 'description' => __( 'PHP date format string', 'airo-wp' ), + ), + 'time_format' => array( + 'type' => 'string', + 'description' => __( 'PHP time format string', 'airo-wp' ), + ), + 'posts_per_page' => array( + 'type' => 'integer', + 'description' => __( 'Number of posts to show per page', 'airo-wp' ), + ), + 'blog_public' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the site is visible to search engines', 'airo-wp' ), + ), + 'site_logo' => array( + 'type' => array( 'integer', 'null' ), + 'description' => __( 'Media attachment ID for site logo, or null if not set', 'airo-wp' ), + ), + 'site_icon' => array( + 'type' => array( 'integer', 'null' ), + 'description' => __( 'Media attachment ID for site icon (favicon), or null if not set', 'airo-wp' ), + ), + 'stats' => array( + 'type' => 'object', + 'properties' => array( + 'post_count' => array( 'type' => 'integer' ), + 'page_count' => array( 'type' => 'integer' ), + ), + ), + 'theme_info' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + 'author' => array( 'type' => 'string' ), + ), + ), + 'plugin_count' => array( + 'type' => 'integer', + 'description' => __( 'Number of active plugins', 'airo-wp' ), + ), + 'reading_settings' => array( + 'type' => 'object', + 'properties' => array( + 'show_on_front' => array( + 'type' => 'string', + 'enum' => array( 'posts', 'page' ), + ), + 'page_on_front' => array( 'type' => array( 'integer', 'null' ) ), + 'page_for_posts' => array( 'type' => array( 'integer', 'null' ) ), + ), + ), + ), + ); + } + + /** + * Published post count for a post type. + * + * @param string $post_type Post type slug. + * @return int + */ + private function get_published_post_count( string $post_type ): int { + $counts = wp_count_posts( $post_type ); + return isset( $counts->publish ) ? (int) $counts->publish : 0; + } + + /** + * Active theme name, version, author. + * + * @return array + */ + private function get_theme_info(): array { + $theme = wp_get_theme(); + return array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'author' => $theme->get( 'Author' ), + ); + } + + /** + * Count of active plugins (single-site and network-activated). + * + * @return int + */ + private function get_active_plugin_count(): int { + $active_plugins = get_option( 'active_plugins', array() ); + if ( is_multisite() ) { + $network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); + $active_plugins = array_merge( $active_plugins, $network_plugins ); + } + return count( array_unique( $active_plugins ) ); + } + + /** + * Reading settings (front page display, blog page). + * + * @return array + */ + private function get_reading_settings(): array { + $show_on_front = get_option( 'show_on_front', 'posts' ); + $shows_page_on_front = 'page' === $show_on_front; + // IDs only meaningful when a static page is shown; zero-out otherwise so the > 0 + // guard below produces null consistently regardless of any stale option values. + $page_on_front = $shows_page_on_front ? (int) get_option( 'page_on_front', 0 ) : 0; + $page_for_posts = $shows_page_on_front ? (int) get_option( 'page_for_posts', 0 ) : 0; + + return array( + 'show_on_front' => $show_on_front, + 'page_on_front' => $page_on_front > 0 ? $page_on_front : null, + 'page_for_posts' => $page_for_posts > 0 ? $page_for_posts : null, + ); + } +} diff --git a/includes/Mcp/Tools/Site/UpdateSiteOptions.php b/includes/Mcp/Tools/Site/UpdateSiteOptions.php new file mode 100644 index 0000000..d10929e --- /dev/null +++ b/includes/Mcp/Tools/Site/UpdateSiteOptions.php @@ -0,0 +1,409 @@ + + */ + private const PROTECTED_OPTIONS = array( + 'db_version', + 'wp_db_version', + 'secret_key', + 'auth_key', + 'secure_auth_key', + 'logged_in_key', + 'nonce_key', + 'auth_salt', + 'secure_auth_salt', + 'logged_in_salt', + 'nonce_salt', + ); + + /** + * Permission callback for wp_register_ability. + * + * @return bool Whether the current user has the required capability. + */ + public function check_permissions(): bool { + return current_user_can( 'manage_options' ); + } + + /** + * Register this tool as a WordPress ability. + */ + public function register(): void { + wp_register_ability( + self::TOOL_ID, + array( + 'label' => __( 'Update Site Options', 'airo-wp' ), + 'description' => __( 'Updates one or more WordPress site options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->build_output_schema( + __( 'Site options update result', 'airo-wp' ), + array( + 'updated_count' => array( + 'type' => 'integer', + 'description' => __( 'Number of options successfully updated', 'airo-wp' ), + ), + 'failed_count' => array( + 'type' => 'integer', + 'description' => __( 'Number of options that failed to update', 'airo-wp' ), + ), + 'results' => array( + 'type' => 'array', + 'description' => __( 'Per-option results', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'option_name' => array( 'type' => 'string' ), + 'success' => array( 'type' => 'boolean' ), + 'message' => array( 'type' => 'string' ), + ), + ), + ), + ), + array( 'updated_count', 'failed_count', 'results' ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'site-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $has_single = array_key_exists( 'option_name', $input ); + $has_bulk = ! empty( $input['options'] ) && is_array( $input['options'] ); + + // Reject if both single and bulk are provided. + if ( $has_single && $has_bulk ) { + return array( + 'success' => false, + 'message' => __( 'Provide either option_name/option_value or options, not both.', 'airo-wp' ), + ); + } + + // Normalize into array of items. + $items = array(); + + if ( $has_single ) { + $name = sanitize_text_field( (string) $input['option_name'] ); + if ( empty( $name ) ) { + return array( + 'success' => false, + 'message' => __( 'Option name cannot be empty.', 'airo-wp' ), + ); + } + $items[] = array( + 'option_name' => $input['option_name'], + 'option_value' => array_key_exists( 'option_value', $input ) ? $input['option_value'] : null, + ); + } elseif ( $has_bulk ) { + $items = $input['options']; + } + + if ( empty( $items ) ) { + return array( + 'success' => false, + 'message' => __( 'No valid options provided.', 'airo-wp' ), + ); + } + + // Pre-validate entire batch (fail closed, no partial writes). + foreach ( $items as $item ) { + $name = isset( $item['option_name'] ) ? sanitize_text_field( $item['option_name'] ) : ''; + $name = $this->canonical_option_name( $name ); + + if ( empty( $name ) ) { + return array( + 'success' => false, + 'message' => __( 'Option name cannot be empty.', 'airo-wp' ), + ); + } + + if ( $this->is_protected_option( $name ) ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: option name */ + __( 'Option "%s" is protected and cannot be modified.', 'airo-wp' ), + $name + ), + ); + } + } + + // Process updates. + $results = array(); + $updated_count = 0; + $failed_count = 0; + + foreach ( $items as $item ) { + $name = $this->canonical_option_name( sanitize_text_field( $item['option_name'] ) ); + $value = array_key_exists( 'option_value', $item ) ? $item['option_value'] : null; + + // WPLANG special handling. + if ( 'WPLANG' === $name ) { + if ( ! is_string( $value ) && ! is_null( $value ) ) { + $results[] = array( + 'option_name' => $name, + 'success' => false, + 'message' => __( 'WPLANG value must be a string or null.', 'airo-wp' ), + ); + ++$failed_count; + continue; + } + $value = $this->normalize_wplang_value( $value ); + $this->ensure_core_language_pack_for_locale( $value ); + // Language pack failure is non-blocking; we still attempt the update. + } + + $updated = update_option( $name, $value ); + + if ( false === $updated ) { + // Check if value is already the same (treat as success). + $current = get_option( $name ); + if ( $this->values_are_equal( $current, $value ) ) { + $results[] = array( + 'option_name' => $name, + 'success' => true, + 'message' => __( 'Option value unchanged (already set to this value).', 'airo-wp' ), + ); + ++$updated_count; + } else { + $results[] = array( + 'option_name' => $name, + 'success' => false, + 'message' => __( 'Failed to update option.', 'airo-wp' ), + ); + ++$failed_count; + } + } else { + $results[] = array( + 'option_name' => $name, + 'success' => true, + 'message' => __( 'Option updated successfully.', 'airo-wp' ), + ); + ++$updated_count; + } + } + + $all_success = 0 === $failed_count; + + return array( + 'success' => $all_success, + 'message' => $all_success + ? sprintf( + /* translators: %d: number of options updated */ + __( 'Successfully updated %d option(s).', 'airo-wp' ), + $updated_count + ) + : sprintf( + /* translators: 1: number of failures, 2: total options */ + __( '%1$d of %2$d option(s) failed to update.', 'airo-wp' ), + $failed_count, + count( $items ) + ), + 'updated_count' => $updated_count, + 'failed_count' => $failed_count, + 'results' => $results, + ); + } + + /** + * Input JSON Schema for this tool. + * + * @return array + */ + private function get_input_schema(): array { + $option_value_schema = array( + 'description' => __( 'The option value', 'airo-wp' ), + 'anyOf' => array( + array( 'type' => 'string' ), + array( 'type' => 'number' ), + array( 'type' => 'boolean' ), + array( 'type' => 'array' ), + array( 'type' => 'object' ), + array( 'type' => 'null' ), + ), + ); + + return array( + 'type' => 'object', + 'properties' => array( + 'option_name' => array( + 'type' => 'string', + 'description' => __( 'Option name for single option updates', 'airo-wp' ), + ), + 'option_value' => $option_value_schema, + 'options' => array( + 'type' => 'array', + 'description' => __( 'Array of options for bulk updates', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'option_name' => array( + 'type' => 'string', + 'description' => __( 'The option name', 'airo-wp' ), + ), + 'option_value' => $option_value_schema, + ), + 'required' => array( 'option_name', 'option_value' ), + ), + ), + ), + 'anyOf' => array( + array( 'required' => array( 'option_name', 'option_value' ) ), + array( 'required' => array( 'options' ) ), + ), + ); + } + + /** + * Canonicalize option name. + * + * Maps case-insensitive 'wplang' to 'WPLANG'. + * + * @param string $name Option name. + * @return string Canonicalized option name. + */ + private function canonical_option_name( string $name ): string { + if ( 'wplang' === strtolower( $name ) ) { + return 'WPLANG'; + } + + return $name; + } + + /** + * Check if an option is protected. + * + * @param string $name Option name. + * @return bool True if the option is protected. + */ + private function is_protected_option( string $name ): bool { + return in_array( strtolower( $name ), self::PROTECTED_OPTIONS, true ); + } + + /** + * Normalize a WPLANG value. + * + * Converts null to empty string, trims, replaces '-' with '_', + * and converts 'en_US' to empty string (WordPress default). + * + * @param string|null $value WPLANG value. + * @return string Normalized locale string. + */ + private function normalize_wplang_value( $value ): string { + if ( null === $value ) { + return ''; + } + + $value = trim( $value ); + $value = str_replace( '-', '_', $value ); + + if ( 'en_US' === $value ) { + return ''; + } + + return $value; + } + + /** + * Ensure a language pack is available for the given locale. + * + * @param string $locale Locale string. + * @return array{success: bool, message: string} Result of language pack installation. + */ + private function ensure_core_language_pack_for_locale( string $locale ): array { + if ( empty( $locale ) ) { + return array( + 'success' => true, + 'message' => __( 'Default locale (en_US) does not need a language pack.', 'airo-wp' ), + ); + } + + $this->load_admin_file( 'file.php' ); + $this->load_admin_file( 'translation-install.php' ); + + $result = wp_download_language_pack( $locale ); + + if ( false === $result ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: locale */ + __( 'Failed to download language pack for "%s".', 'airo-wp' ), + $locale + ), + ); + } + + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: locale */ + __( 'Language pack for "%s" installed successfully.', 'airo-wp' ), + $locale + ), + ); + } + + /** + * Compare two values for equality, handling boolean normalization. + * + * WordPress stores booleans as '1'/'0' strings, so we normalize + * before comparison. + * + * @param mixed $current Current option value. + * @param mixed $new_value New option value. + * @return bool True if values are considered equal. + */ + private function values_are_equal( $current, $new_value ): bool { + // Normalize booleans for comparison. + if ( is_bool( $new_value ) ) { + $new_value = $new_value ? '1' : '0'; + } + + if ( is_bool( $current ) ) { + $current = $current ? '1' : '0'; + } + + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison, Universal.Operators.StrictComparisons.LooseEqual -- Intentional loose comparison for option values. + return $current == $new_value; + } +} diff --git a/includes/Mcp/Tools/Templates/DeleteTemplate.php b/includes/Mcp/Tools/Templates/DeleteTemplate.php new file mode 100644 index 0000000..303b020 --- /dev/null +++ b/includes/Mcp/Tools/Templates/DeleteTemplate.php @@ -0,0 +1,211 @@ + __( 'Delete Template', 'airo-wp' ), + 'description' => __( 'Deletes a block template (resets to theme default by removing DB override)', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the delete template tool. + * + * Uses WordPress core REST API controller for template operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Input parameters. + * @return array Deletion result or error. + */ + public function execute( array $input ): array { + try { + $template_id = isset( $input['id'] ) ? sanitize_text_field( $input['id'] ) : ''; + $force = isset( $input['force'] ) ? (bool) $input['force'] : false; + + if ( empty( $template_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Template ID is required', 'airo-wp' ), + ); + } + + // Check if template exists using WordPress core function. + $existing_template = get_block_template( $template_id, 'wp_template' ); + + if ( ! $existing_template ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: template ID */ + __( 'Template "%s" not found', 'airo-wp' ), + $template_id + ), + ); + } + + // Store previous data before deletion. + $previous = array( + 'id' => $existing_template->id, + 'slug' => $existing_template->slug, + 'theme' => $existing_template->theme, + 'source' => $existing_template->source, + 'title' => $existing_template->title, + 'description' => $existing_template->description, + 'wp_id' => isset( $existing_template->wp_id ) ? (int) $existing_template->wp_id : null, + ); + + // Check if this is a theme-only template (no DB override). + // If source is 'theme' and there's no wp_id, it's theme-only. + if ( 'theme' === $existing_template->source && empty( $existing_template->wp_id ) ) { + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: template ID */ + __( 'Template "%s" is theme-only (no customizations to reset)', 'airo-wp' ), + $template_id + ), + 'deleted' => array( + 'id' => $template_id, + 'status' => 'theme-only', + ), + ); + } + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'DELETE', '/wp/v2/templates/' . $template_id ); + $request->set_param( 'id', $template_id ); + $request->set_param( 'force', $force ); + + // Use WordPress REST controller. + $controller = new \WP_REST_Templates_Controller( 'wp_template' ); + $response = $controller->delete_item( $request ); + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: template ID */ + __( 'Template "%s" reset to theme default', 'airo-wp' ), + $template_id + ), + 'deleted' => array( + 'id' => $template_id, + 'status' => 'reset', + ), + 'previous' => $previous, + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error deleting template: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'Template ID in format theme//slug (e.g., "twentytwentyfive//page")', 'airo-wp' ), + ), + 'force' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to force deletion (bypass trash)', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template deletion result', 'airo-wp' ), + array( + 'deleted' => array( + 'type' => 'object', + 'description' => __( 'Information about the deleted template', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'The deleted template ID', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The previous status of the template', 'airo-wp' ), + ), + ), + ), + 'previous' => array( + 'type' => 'object', + 'description' => __( 'The template data before deletion', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/DeleteTemplatePart.php b/includes/Mcp/Tools/Templates/DeleteTemplatePart.php new file mode 100644 index 0000000..16d1e90 --- /dev/null +++ b/includes/Mcp/Tools/Templates/DeleteTemplatePart.php @@ -0,0 +1,212 @@ + __( 'Delete Template Part', 'airo-wp' ), + 'description' => __( 'Deletes a block template part (resets to theme default by removing DB override)', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the delete template part tool. + * + * Uses WordPress core REST API controller for template part operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Input parameters. + * @return array Deletion result or error. + */ + public function execute( array $input ): array { + try { + $template_part_id = isset( $input['id'] ) ? sanitize_text_field( $input['id'] ) : ''; + $force = isset( $input['force'] ) ? (bool) $input['force'] : false; + + if ( empty( $template_part_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Template part ID is required', 'airo-wp' ), + ); + } + + // Check if template part exists using WordPress core function. + $existing_template_part = get_block_template( $template_part_id, 'wp_template_part' ); + + if ( ! $existing_template_part ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: template part ID */ + __( 'Template part "%s" not found', 'airo-wp' ), + $template_part_id + ), + ); + } + + // Store previous data before deletion. + $previous = array( + 'id' => $existing_template_part->id, + 'slug' => $existing_template_part->slug, + 'theme' => $existing_template_part->theme, + 'source' => $existing_template_part->source, + 'title' => $existing_template_part->title, + 'description' => $existing_template_part->description, + 'area' => $existing_template_part->area ?? 'uncategorized', + 'wp_id' => isset( $existing_template_part->wp_id ) ? (int) $existing_template_part->wp_id : null, + ); + + // Check if this is a theme-only template part (no DB override). + // If source is 'theme' and there's no wp_id, it's theme-only. + if ( 'theme' === $existing_template_part->source && empty( $existing_template_part->wp_id ) ) { + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: template part ID */ + __( 'Template part "%s" is theme-only (no customizations to reset)', 'airo-wp' ), + $template_part_id + ), + 'deleted' => array( + 'id' => $template_part_id, + 'status' => 'theme-only', + ), + ); + } + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'DELETE', '/wp/v2/template-parts/' . $template_part_id ); + $request->set_param( 'id', $template_part_id ); + $request->set_param( 'force', $force ); + + // Use WordPress REST controller. + $controller = new \WP_REST_Templates_Controller( 'wp_template_part' ); + $response = $controller->delete_item( $request ); + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + return array( + 'success' => true, + 'message' => sprintf( + /* translators: %s: template part ID */ + __( 'Template part "%s" reset to theme default', 'airo-wp' ), + $template_part_id + ), + 'deleted' => array( + 'id' => $template_part_id, + 'status' => 'reset', + ), + 'previous' => $previous, + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error deleting template part: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'Template part ID in format theme//slug (e.g., "twentytwentyfive//header")', 'airo-wp' ), + ), + 'force' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to force deletion (bypass trash)', 'airo-wp' ), + 'default' => false, + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template part deletion result', 'airo-wp' ), + array( + 'deleted' => array( + 'type' => 'object', + 'description' => __( 'Information about the deleted template part', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'The deleted template part ID', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'The previous status of the template part', 'airo-wp' ), + ), + ), + ), + 'previous' => array( + 'type' => 'object', + 'description' => __( 'The template part data before deletion', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/ListTemplatePartRevisions.php b/includes/Mcp/Tools/Templates/ListTemplatePartRevisions.php new file mode 100644 index 0000000..49fd19c --- /dev/null +++ b/includes/Mcp/Tools/Templates/ListTemplatePartRevisions.php @@ -0,0 +1,224 @@ + __( 'List Template Part Revisions', 'airo-wp' ), + 'description' => __( 'Retrieves revisions for a block template part', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the list template part revisions tool. + * + * Uses WordPress core REST API controller for template part revision operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Input parameters. + * @return array List of revisions or error. + */ + public function execute( array $input ): array { + try { + $template_part_id = isset( $input['id'] ) ? sanitize_text_field( $input['id'] ) : ''; + + if ( empty( $template_part_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Template part ID is required', 'airo-wp' ), + ); + } + + // Get the template part to find its wp_id. + $template_part = get_block_template( $template_part_id, 'wp_template_part' ); + + if ( ! $template_part ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: template part ID */ + __( 'Template part "%s" not found', 'airo-wp' ), + $template_part_id + ), + ); + } + + if ( empty( $template_part->wp_id ) ) { + return array( + 'success' => true, + 'revisions' => array(), + 'total' => 0, + 'message' => sprintf( + /* translators: %s: template part ID */ + __( 'No revisions found for template part "%s"', 'airo-wp' ), + $template_part_id + ), + ); + } + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'GET', '/wp/v2/template-parts/' . $template_part_id . '/revisions' ); + $request->set_param( 'parent', $template_part->wp_id ); + $request->set_param( 'per_page', isset( $input['per_page'] ) ? (int) $input['per_page'] : 10 ); + $request->set_param( 'page', isset( $input['page'] ) ? (int) $input['page'] : 1 ); + + // Use WordPress REST controller for revisions. + $controller = new \WP_REST_Revisions_Controller( 'wp_template_part' ); + $response = $controller->get_items( $request ); + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $revisions = $response->get_data(); + + return array( + 'success' => true, + 'revisions' => $revisions, + 'total' => count( $revisions ), + 'total_pages' => 1, + 'message' => sprintf( + /* translators: %1$d: number of revisions, %2$s: template part ID */ + __( 'Retrieved %1$d revision(s) for template part "%2$s"', 'airo-wp' ), + count( $revisions ), + $template_part_id + ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error listing template part revisions: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'Template part ID in format theme//slug', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to return', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template part revisions list result', 'airo-wp' ), + array( + 'revisions' => array( + 'type' => 'array', + 'description' => __( 'Array of revision objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Revision ID', 'airo-wp' ), + ), + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'Parent template part wp_id', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Author ID', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'Revision date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'Revision slug', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of revisions', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/ListTemplateParts.php b/includes/Mcp/Tools/Templates/ListTemplateParts.php new file mode 100644 index 0000000..e182935 --- /dev/null +++ b/includes/Mcp/Tools/Templates/ListTemplateParts.php @@ -0,0 +1,231 @@ + __( 'List Template Parts', 'airo-wp' ), + 'description' => __( 'Retrieves a list of block template parts with filtering options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the list template parts tool. + * + * Uses WordPress core REST API controller for template part operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Input parameters. + * @return array List of template parts or error. + */ + public function execute( array $input ): array { + try { + $context = isset( $input['context'] ) ? $input['context'] : 'edit'; + $wp_id = isset( $input['wp_id'] ) ? (int) $input['wp_id'] : null; + $area = isset( $input['area'] ) ? sanitize_text_field( $input['area'] ) : null; + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'GET', '/wp/v2/template-parts' ); + $request->set_param( 'context', $context ); + if ( $wp_id ) { + $request->set_param( 'wp_id', $wp_id ); + } + if ( $area ) { + $request->set_param( 'area', $area ); + } + + // Use WordPress REST controller. + $controller = new \WP_REST_Templates_Controller( 'wp_template_part' ); + $response = $controller->get_items( $request ); + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $template_parts = $response->get_data(); + foreach ( $template_parts as &$template_part ) { + if ( array_key_exists( 'origin', $template_part ) && ! is_string( $template_part['origin'] ) ) { + unset( $template_part['origin'] ); + } + if ( array_key_exists( 'modified', $template_part ) && ! is_string( $template_part['modified'] ) ) { + unset( $template_part['modified'] ); + } + } + unset( $template_part ); + + return array( + 'success' => true, + 'template_parts' => $template_parts, + 'message' => sprintf( + /* translators: %d: number of template parts */ + __( 'Retrieved %d template part(s)', 'airo-wp' ), + count( $template_parts ) + ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error listing template parts: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'edit', + ), + 'wp_id' => array( + 'type' => 'integer', + 'description' => __( 'Limit to the specified post ID (numeric wp_posts.ID)', 'airo-wp' ), + ), + 'area' => array( + 'type' => 'string', + 'description' => __( 'Limit to the specified template part area (header, footer, sidebar, uncategorized)', 'airo-wp' ), + 'enum' => array( 'header', 'footer', 'sidebar', 'uncategorized' ), + ), + ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template parts list result', 'airo-wp' ), + array( + 'template_parts' => array( + 'type' => 'array', + 'description' => __( 'Array of template part objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'Template part ID in format theme//slug', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'Template part slug', 'airo-wp' ), + ), + 'theme' => array( + 'type' => 'string', + 'description' => __( 'Theme slug', 'airo-wp' ), + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Post type (wp_template_part)', 'airo-wp' ), + ), + 'source' => array( + 'type' => 'string', + 'description' => __( 'Source of the template part (theme, custom, plugin)', 'airo-wp' ), + ), + 'origin' => array( + 'type' => 'string', + 'description' => __( 'Theme/plugin that originally provided the template part', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'object', + 'description' => __( 'Template part content', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'object', + 'description' => __( 'Template part title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'Template part description', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Template part status', 'airo-wp' ), + ), + 'wp_id' => array( + 'type' => 'integer', + 'description' => __( 'Numeric post ID in wp_posts table', 'airo-wp' ), + ), + 'has_theme_file' => array( + 'type' => 'boolean', + 'description' => __( 'Whether a theme file exists for this template part', 'airo-wp' ), + ), + 'area' => array( + 'type' => 'string', + 'description' => __( 'Template part area (header, footer, sidebar, uncategorized)', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Author ID', 'airo-wp' ), + ), + 'modified' => array( + 'type' => 'string', + 'description' => __( 'Last modified date', 'airo-wp' ), + ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/ListTemplateRevisions.php b/includes/Mcp/Tools/Templates/ListTemplateRevisions.php new file mode 100644 index 0000000..3078c08 --- /dev/null +++ b/includes/Mcp/Tools/Templates/ListTemplateRevisions.php @@ -0,0 +1,224 @@ + __( 'List Template Revisions', 'airo-wp' ), + 'description' => __( 'Retrieves revisions for a block template', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the list template revisions tool. + * + * Uses WordPress core REST API controller for template revision operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Input parameters. + * @return array List of revisions or error. + */ + public function execute( array $input ): array { + try { + $template_id = isset( $input['id'] ) ? sanitize_text_field( $input['id'] ) : ''; + + if ( empty( $template_id ) ) { + return array( + 'success' => false, + 'message' => __( 'Template ID is required', 'airo-wp' ), + ); + } + + // Get the template to find its wp_id. + $template = get_block_template( $template_id, 'wp_template' ); + + if ( ! $template ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: template ID */ + __( 'Template "%s" not found', 'airo-wp' ), + $template_id + ), + ); + } + + if ( empty( $template->wp_id ) ) { + return array( + 'success' => true, + 'revisions' => array(), + 'total' => 0, + 'message' => sprintf( + /* translators: %s: template ID */ + __( 'No revisions found for template "%s"', 'airo-wp' ), + $template_id + ), + ); + } + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'GET', '/wp/v2/templates/' . $template_id . '/revisions' ); + $request->set_param( 'parent', $template->wp_id ); + $request->set_param( 'per_page', isset( $input['per_page'] ) ? (int) $input['per_page'] : 10 ); + $request->set_param( 'page', isset( $input['page'] ) ? (int) $input['page'] : 1 ); + + // Use WordPress REST controller for revisions. + $controller = new \WP_REST_Revisions_Controller( 'wp_template' ); + $response = $controller->get_items( $request ); + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $revisions = $response->get_data(); + + return array( + 'success' => true, + 'revisions' => $revisions, + 'total' => count( $revisions ), + 'total_pages' => 1, + 'message' => sprintf( + /* translators: %1$d: number of revisions, %2$s: template ID */ + __( 'Retrieved %1$d revision(s) for template "%2$s"', 'airo-wp' ), + count( $revisions ), + $template_id + ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error listing template revisions: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'Template ID in format theme//slug', 'airo-wp' ), + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Maximum number of items to return', 'airo-wp' ), + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Current page of the collection', 'airo-wp' ), + 'default' => 1, + 'minimum' => 1, + ), + ), + 'required' => array( 'id' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template revisions list result', 'airo-wp' ), + array( + 'revisions' => array( + 'type' => 'array', + 'description' => __( 'Array of revision objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'Revision ID', 'airo-wp' ), + ), + 'parent' => array( + 'type' => 'integer', + 'description' => __( 'Parent template wp_id', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Author ID', 'airo-wp' ), + ), + 'date' => array( + 'type' => 'string', + 'description' => __( 'Revision date', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'Revision slug', 'airo-wp' ), + ), + ), + ), + ), + 'total' => array( + 'type' => 'integer', + 'description' => __( 'Total number of revisions', 'airo-wp' ), + ), + 'total_pages' => array( + 'type' => 'integer', + 'description' => __( 'Total number of pages', 'airo-wp' ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/ListTemplates.php b/includes/Mcp/Tools/Templates/ListTemplates.php new file mode 100644 index 0000000..4d4dfde --- /dev/null +++ b/includes/Mcp/Tools/Templates/ListTemplates.php @@ -0,0 +1,222 @@ + __( 'List Templates', 'airo-wp' ), + 'description' => __( 'Retrieves a list of block templates with filtering options', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the list templates tool. + * + * Uses WordPress core REST API controller for template operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Input parameters. + * @return array List of templates or error. + */ + public function execute( array $input ): array { + try { + $context = isset( $input['context'] ) ? $input['context'] : 'edit'; + $wp_id = isset( $input['wp_id'] ) ? (int) $input['wp_id'] : null; + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'GET', '/wp/v2/templates' ); + $request->set_param( 'context', $context ); + if ( $wp_id ) { + $request->set_param( 'wp_id', $wp_id ); + } + + // Use WordPress REST controller. + $controller = new \WP_REST_Templates_Controller( 'wp_template' ); + $response = $controller->get_items( $request ); + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $templates = $response->get_data(); + foreach ( $templates as &$template ) { + if ( array_key_exists( 'origin', $template ) && ! is_string( $template['origin'] ) ) { + unset( $template['origin'] ); + } + if ( array_key_exists( 'modified', $template ) && ! is_string( $template['modified'] ) ) { + unset( $template['modified'] ); + } + } + unset( $template ); + + return array( + 'success' => true, + 'templates' => $templates, + 'message' => sprintf( + /* translators: %d: number of templates */ + __( 'Retrieved %d template(s)', 'airo-wp' ), + count( $templates ) + ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error listing templates: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'context' => array( + 'type' => 'string', + 'description' => __( 'Scope under which the request is made; determines fields present in response', 'airo-wp' ), + 'enum' => array( 'view', 'embed', 'edit' ), + 'default' => 'edit', + ), + 'wp_id' => array( + 'type' => 'integer', + 'description' => __( 'Limit to the specified post ID (numeric wp_posts.ID)', 'airo-wp' ), + ), + ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Templates list result', 'airo-wp' ), + array( + 'templates' => array( + 'type' => 'array', + 'description' => __( 'Array of template objects', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'Template ID in format theme//slug', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'Template slug', 'airo-wp' ), + ), + 'theme' => array( + 'type' => 'string', + 'description' => __( 'Theme slug', 'airo-wp' ), + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Post type (wp_template)', 'airo-wp' ), + ), + 'source' => array( + 'type' => 'string', + 'description' => __( 'Source of the template (theme, custom, plugin)', 'airo-wp' ), + ), + 'origin' => array( + 'type' => 'string', + 'description' => __( 'Theme/plugin that originally provided the template', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'object', + 'description' => __( 'Template content', 'airo-wp' ), + ), + 'title' => array( + 'type' => 'object', + 'description' => __( 'Template title', 'airo-wp' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'Template description', 'airo-wp' ), + ), + 'status' => array( + 'type' => 'string', + 'description' => __( 'Template status', 'airo-wp' ), + ), + 'wp_id' => array( + 'type' => 'integer', + 'description' => __( 'Numeric post ID in wp_posts table', 'airo-wp' ), + ), + 'has_theme_file' => array( + 'type' => 'boolean', + 'description' => __( 'Whether a theme file exists for this template', 'airo-wp' ), + ), + 'is_custom' => array( + 'type' => 'boolean', + 'description' => __( 'Whether this is a user-customized template', 'airo-wp' ), + ), + 'author' => array( + 'type' => 'integer', + 'description' => __( 'Author ID', 'airo-wp' ), + ), + 'modified' => array( + 'type' => 'string', + 'description' => __( 'Last modified date', 'airo-wp' ), + ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/UpdateTemplate.php b/includes/Mcp/Tools/Templates/UpdateTemplate.php new file mode 100644 index 0000000..b9f2a83 --- /dev/null +++ b/includes/Mcp/Tools/Templates/UpdateTemplate.php @@ -0,0 +1,238 @@ + __( 'Update Template', 'airo-wp' ), + 'description' => __( 'Updates a template in the database with new HTML content. Creates the template if it does not exist.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * Uses WordPress core REST API controller for template operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Tool input parameters. + * @return array + */ + public function execute( array $input ): array { + try { + // Validate required parameters. + $theme = isset( $input['theme'] ) ? sanitize_text_field( $input['theme'] ) : ''; + $template_id = isset( $input['id'] ) ? sanitize_text_field( $input['id'] ) : ''; + $template_name = isset( $input['template_name'] ) ? sanitize_text_field( $input['template_name'] ) : ''; + $html_content = isset( $input['html'] ) ? $input['html'] : ''; + + if ( empty( $theme ) ) { + return array( + 'success' => false, + 'message' => __( 'Theme parameter is required', 'airo-wp' ), + ); + } + + if ( empty( $template_id ) && empty( $template_name ) ) { + return array( + 'success' => false, + 'message' => __( 'Either template ID or template_name is required', 'airo-wp' ), + ); + } + + if ( empty( $html_content ) ) { + return array( + 'success' => false, + 'message' => __( 'HTML content is required', 'airo-wp' ), + ); + } + + if ( ! empty( $template_id ) ) { + $wp_template_id = (string) $template_id; + // Extract slug from template ID. + $parts = explode( '//', $wp_template_id ); + $slug = end( $parts ); + } else { + // Build template ID in WordPress format: theme//slug. + $wp_template_id = $theme . '//' . $template_name; + $slug = $template_name; + } + + /* + * Use WordPress core function to check if template exists. + * This handles both database and file system lookups automatically. + */ + $existing_template = get_block_template( $wp_template_id, 'wp_template' ); + + $was_already_db_backed = $existing_template && ! empty( $existing_template->wp_id ); + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_param( 'id', $wp_template_id ); + $request->set_param( 'theme', $theme ); + $request->set_param( 'slug', $slug ); + $request->set_param( 'content', $html_content ); + + /* + * Use WordPress REST controller for create/update operations. + * Permission checks pass automatically because we're running as admin + * via the permission_callback. + */ + $controller = new \WP_REST_Templates_Controller( 'wp_template' ); + + if ( $existing_template ) { + // Update existing template using WordPress core. + $response = $controller->update_item( $request ); + } else { + // Create new template using WordPress core. + $response = $controller->create_item( $request ); + } + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $data = $response->get_data(); + $wp_id = isset( $data['wp_id'] ) ? (int) $data['wp_id'] : 0; + + if ( ! $was_already_db_backed && $wp_id > 0 ) { + wp_save_post_revision( $wp_id ); + } + + return array( + 'success' => true, + 'data' => array( + 'id' => $data['id'], + 'slug' => $data['slug'], + 'theme' => $data['theme'], + 'wp_id' => $wp_id > 0 ? $wp_id : null, + 'content' => $data['content']['raw'], + ), + 'message' => __( 'Template updated successfully', 'airo-wp' ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error updating template: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug where the template belongs', 'airo-wp' ), + ), + 'id' => array( + 'type' => 'string', + 'description' => __( 'The template ID (optional if template_name is provided)', 'airo-wp' ), + ), + 'template_name' => array( + 'type' => 'string', + 'description' => __( 'The template name/slug (optional if id is provided, e.g., "page", "single")', 'airo-wp' ), + ), + 'html' => array( + 'type' => 'string', + 'description' => __( 'The new HTML content for the template', 'airo-wp' ), + ), + ), + 'required' => array( 'theme', 'html' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template update result', 'airo-wp' ), + array( + 'data' => array( + 'type' => 'object', + 'description' => __( 'Template data', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'The template ID', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The template slug', 'airo-wp' ), + ), + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug', 'airo-wp' ), + ), + 'wp_id' => array( + 'type' => array( 'integer', 'null' ), + 'description' => __( 'Numeric post ID in wp_posts table; used for revision operations. Null when not yet persisted as a DB override.', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The updated HTML content', 'airo-wp' ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Templates/UpdateTemplatePart.php b/includes/Mcp/Tools/Templates/UpdateTemplatePart.php new file mode 100644 index 0000000..fb2c30d --- /dev/null +++ b/includes/Mcp/Tools/Templates/UpdateTemplatePart.php @@ -0,0 +1,257 @@ + __( 'Update Template Part', 'airo-wp' ), + 'description' => __( 'Updates a template part in the database with new HTML content. Creates the template part if it does not exist.', 'airo-wp' ), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * Uses WordPress core REST API controller for template part operations. + * Permissions are handled automatically via the permission_callback. + * + * @param array $input Tool input parameters. + * @return array + */ + public function execute( array $input ): array { + try { + // Validate required parameters. + $theme = isset( $input['theme'] ) ? sanitize_text_field( $input['theme'] ) : ''; + $template_id = isset( $input['id'] ) ? sanitize_text_field( $input['id'] ) : ''; + $part_name = isset( $input['part_name'] ) ? sanitize_text_field( $input['part_name'] ) : ''; + $html_content = isset( $input['html'] ) ? $input['html'] : ''; + + if ( empty( $theme ) ) { + return array( + 'success' => false, + 'message' => __( 'Theme parameter is required', 'airo-wp' ), + ); + } + + if ( empty( $template_id ) && empty( $part_name ) ) { + return array( + 'success' => false, + 'message' => __( 'Either template part ID or part_name is required', 'airo-wp' ), + ); + } + + if ( empty( $html_content ) ) { + return array( + 'success' => false, + 'message' => __( 'HTML content is required', 'airo-wp' ), + ); + } + + if ( ! empty( $template_id ) ) { + $wp_template_id = (string) $template_id; + // Extract slug from template ID. + $parts = explode( '//', $wp_template_id ); + $slug = end( $parts ); + } else { + // Build template ID in WordPress format: theme//slug. + $wp_template_id = $theme . '//' . $part_name; + $slug = $part_name; + } + + /* + * Use WordPress core function to check if template exists. + * This handles both database and file system lookups automatically. + */ + $existing_template = get_block_template( $wp_template_id, 'wp_template_part' ); + + /* + * Resolve area with input taking precedence, else fall back to the + * existing template part's area so the new DB override does not lose + * the theme-defined area (e.g., default to "uncategorized"). + */ + $resolved_area = ''; + if ( isset( $input['area'] ) && is_string( $input['area'] ) && '' !== $input['area'] ) { + $resolved_area = sanitize_text_field( $input['area'] ); + } elseif ( $existing_template && isset( $existing_template->area ) && is_string( $existing_template->area ) && '' !== $existing_template->area ) { + $resolved_area = $existing_template->area; + } + + $was_already_db_backed = $existing_template && ! empty( $existing_template->wp_id ); + + // Prepare request object for WordPress REST API. + $request = new \WP_REST_Request( 'POST', '/wp/v2/template-parts' ); + $request->set_param( 'id', $wp_template_id ); + $request->set_param( 'theme', $theme ); + $request->set_param( 'slug', $slug ); + if ( '' !== $resolved_area ) { + $request->set_param( 'area', $resolved_area ); + } + $request->set_param( 'content', $html_content ); + + /* + * Use WordPress REST controller for create/update operations. + * Permission checks pass automatically because we're running as admin + * via the permission_callback. + */ + $controller = new \WP_REST_Templates_Controller( 'wp_template_part' ); + + if ( $existing_template ) { + // Update existing template using WordPress core. + $response = $controller->update_item( $request ); + } else { + // Create new template using WordPress core. + $response = $controller->create_item( $request ); + } + + // Handle WordPress REST API errors. + if ( is_wp_error( $response ) ) { + return array( + 'success' => false, + 'message' => $response->get_error_message(), + ); + } + + $data = $response->get_data(); + $wp_id = isset( $data['wp_id'] ) ? (int) $data['wp_id'] : 0; + + if ( ! $was_already_db_backed && $wp_id > 0 ) { + wp_save_post_revision( $wp_id ); + } + + return array( + 'success' => true, + 'data' => array( + 'id' => $data['id'], + 'slug' => $data['slug'], + 'theme' => $data['theme'], + 'wp_id' => $wp_id > 0 ? $wp_id : null, + 'content' => $data['content']['raw'], + ), + 'message' => __( 'Template part updated successfully', 'airo-wp' ), + ); + + } catch ( \Exception $e ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: error message */ + __( 'Error updating template part: %s', 'airo-wp' ), + $e->getMessage() + ), + ); + } + } + + /** + * Get input schema for the tool. + * + * @return array + */ + private function get_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug where the template part belongs', 'airo-wp' ), + ), + 'id' => array( + 'type' => 'string', + 'description' => __( 'The template part ID (optional if part_name is provided)', 'airo-wp' ), + ), + 'part_name' => array( + 'type' => 'string', + 'description' => __( 'The template part name/slug (optional if id is provided, e.g., "header", "footer")', 'airo-wp' ), + ), + 'area' => array( + 'type' => 'string', + 'description' => __( 'Template part area (optional, e.g., "header", "footer", "sidebar")', 'airo-wp' ), + ), + 'html' => array( + 'type' => 'string', + 'description' => __( 'The new HTML content for the template part', 'airo-wp' ), + ), + ), + 'required' => array( 'theme', 'html' ), + ); + } + + /** + * Get output schema for the tool. + * + * @return array + */ + private function get_output_schema(): array { + return $this->build_output_schema( + __( 'Template part update result', 'airo-wp' ), + array( + 'data' => array( + 'type' => 'object', + 'description' => __( 'Template part data', 'airo-wp' ), + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'description' => __( 'The template part ID', 'airo-wp' ), + ), + 'slug' => array( + 'type' => 'string', + 'description' => __( 'The template part slug', 'airo-wp' ), + ), + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug', 'airo-wp' ), + ), + 'wp_id' => array( + 'type' => array( 'integer', 'null' ), + 'description' => __( 'Numeric post ID in wp_posts table; used for revision operations. Null when not yet persisted as a DB override.', 'airo-wp' ), + ), + 'content' => array( + 'type' => 'string', + 'description' => __( 'The updated HTML content', 'airo-wp' ), + ), + ), + ), + ) + ); + } +} diff --git a/includes/Mcp/Tools/Themes/ActivateTheme.php b/includes/Mcp/Tools/Themes/ActivateTheme.php new file mode 100644 index 0000000..31f45db --- /dev/null +++ b/includes/Mcp/Tools/Themes/ActivateTheme.php @@ -0,0 +1,222 @@ + __( 'Activate Theme', 'airo-wp' ), + 'description' => __( 'Installs a theme from WordPress.org if not present, then activates it', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'theme_slug' => array( + 'type' => 'string', + 'description' => __( 'The theme slug (e.g. "twentytwentyfour")', 'airo-wp' ), + 'minLength' => 1, + ), + ), + 'required' => array( 'theme_slug' ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Theme activation result', 'airo-wp' ), + array( + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug', 'airo-wp' ), + ), + 'version' => array( + 'type' => 'string', + 'description' => __( 'The activated theme version', 'airo-wp' ), + ), + 'previous_theme' => array( + 'type' => 'string', + 'description' => __( 'The previously active theme slug', 'airo-wp' ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $slug = isset( $input['theme_slug'] ) ? sanitize_text_field( $input['theme_slug'] ) : ''; + + if ( empty( $slug ) ) { + return array( + 'success' => false, + 'message' => __( 'Theme slug is required.', 'airo-wp' ), + ); + } + + $current_theme = get_stylesheet(); + $theme = wp_get_theme( $slug ); + + if ( $theme->exists() ) { + if ( $current_theme === $slug ) { + return array( + 'success' => true, + 'message' => __( 'Theme is already active.', 'airo-wp' ), + 'theme' => $slug, + 'version' => $theme->get( 'Version' ), + 'previous_theme' => $current_theme, + ); + } + + switch_theme( $slug ); + + if ( get_stylesheet() !== $slug ) { + return array( + 'success' => false, + 'message' => __( 'Theme activation failed.', 'airo-wp' ), + 'theme' => $slug, + ); + } + + return array( + 'success' => true, + 'message' => __( 'Theme activated successfully.', 'airo-wp' ), + 'theme' => $slug, + 'version' => $theme->get( 'Version' ), + 'previous_theme' => $current_theme, + ); + } + + // Theme not installed — attempt install. + if ( ! current_user_can( 'install_themes' ) ) { + return array( + 'success' => false, + 'message' => __( 'You do not have permission to install themes.', 'airo-wp' ), + ); + } + + $this->load_admin_file( 'theme.php' ); + $this->load_admin_file( 'file.php' ); + $this->load_admin_file( 'class-wp-upgrader.php' ); + + $api = themes_api( + 'theme_information', + array( + 'slug' => $slug, + 'fields' => array( 'sections' => false ), + ) + ); + + if ( is_wp_error( $api ) ) { + return array( + 'success' => false, + 'message' => $api->get_error_message(), + 'theme' => $slug, + ); + } + + add_filter( 'filesystem_method', array( $this, 'filter_filesystem_method' ) ); + WP_Filesystem(); + + $upgrader = new \Theme_Upgrader( new \Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + + remove_filter( 'filesystem_method', array( $this, 'filter_filesystem_method' ) ); + + if ( is_wp_error( $result ) ) { + return array( + 'success' => false, + 'message' => $result->get_error_message(), + 'theme' => $slug, + ); + } + + if ( ! $result ) { + return array( + 'success' => false, + 'message' => __( 'Theme installation failed.', 'airo-wp' ), + 'theme' => $slug, + ); + } + + // Verify installation and activate. + $theme = wp_get_theme( $slug ); + + if ( ! $theme->exists() ) { + return array( + 'success' => false, + 'message' => __( 'Theme installed but could not be located.', 'airo-wp' ), + 'theme' => $slug, + ); + } + + switch_theme( $slug ); + + if ( get_stylesheet() !== $slug ) { + return array( + 'success' => false, + 'message' => __( 'Theme installed but activation failed.', 'airo-wp' ), + 'theme' => $slug, + ); + } + + return array( + 'success' => true, + 'message' => __( 'Theme installed and activated successfully.', 'airo-wp' ), + 'theme' => $slug, + 'version' => $theme->get( 'Version' ), + 'previous_theme' => $current_theme, + ); + } + + /** + * Filter filesystem method to use direct access. + * + * @return string Filesystem method. + */ + public function filter_filesystem_method(): string { + return 'direct'; + } +} diff --git a/includes/Mcp/Tools/Themes/GetThemes.php b/includes/Mcp/Tools/Themes/GetThemes.php new file mode 100644 index 0000000..411a19d --- /dev/null +++ b/includes/Mcp/Tools/Themes/GetThemes.php @@ -0,0 +1,195 @@ + __( 'Get Themes', 'airo-wp' ), + 'description' => __( 'Retrieves theme information — either the active theme or all installed themes', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'active' => array( + 'type' => 'boolean', + 'description' => __( 'If true, return only the active theme; if false, return all themes', 'airo-wp' ), + ), + ), + 'required' => array( 'active' ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Theme information', 'airo-wp' ), + array( + 'themes' => array( + 'type' => 'array', + 'description' => __( 'List of themes', 'airo-wp' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'title' => array( 'type' => 'string' ), + 'description' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + 'author' => array( 'type' => 'string' ), + 'author_uri' => array( 'type' => 'string' ), + 'theme_uri' => array( 'type' => 'string' ), + 'stylesheet' => array( 'type' => 'string' ), + 'template' => array( 'type' => 'string' ), + 'status' => array( 'type' => 'string' ), + 'tags' => array( 'type' => 'array' ), + 'is_block_theme' => array( 'type' => 'boolean' ), + 'global_styles_id' => array( 'type' => 'integer' ), + ), + ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + if ( ! isset( $input['active'] ) ) { + return array( + 'success' => false, + 'message' => __( 'The active parameter is required.', 'airo-wp' ), + ); + } + + $active = (bool) $input['active']; + $current_theme = get_stylesheet(); + $formatted_themes = array(); + + if ( $active ) { + $theme = wp_get_theme(); + $formatted_themes[] = $this->format_theme_data( $theme, true ); + + $this->ensure_global_styles_post_exists( $theme ); + } else { + $themes = wp_get_themes(); + + foreach ( $themes as $stylesheet => $theme ) { + $is_active = ( $stylesheet === $current_theme ); + $formatted_themes[] = $this->format_theme_data( $theme, $is_active ); + } + } + + return array( + 'success' => true, + 'themes' => $formatted_themes, + 'message' => sprintf( + /* translators: %d: number of themes */ + __( 'Found %d theme(s).', 'airo-wp' ), + count( $formatted_themes ) + ), + ); + } + + /** + * Format theme data into a consistent array structure. + * + * @param \WP_Theme $theme Theme object. + * @param bool $is_active Whether the theme is currently active. + * @return array Formatted theme data. + */ + private function format_theme_data( $theme, bool $is_active ): array { + $is_block_theme = is_callable( array( $theme, 'is_block_theme' ) ) ? $theme->is_block_theme() : false; + + $data = array( + 'name' => $theme->get( 'Name' ), + 'title' => $theme->get( 'Name' ), + 'description' => $theme->get( 'Description' ), + 'version' => $theme->get( 'Version' ), + 'author' => $theme->get( 'Author' ), + 'author_uri' => $theme->get( 'AuthorURI' ), + 'theme_uri' => $theme->get( 'ThemeURI' ), + 'stylesheet' => $theme->get_stylesheet(), + 'template' => $theme->get_template(), + 'status' => $is_active ? 'active' : 'inactive', + 'tags' => $theme->get( 'Tags' ) ? $theme->get( 'Tags' ) : array(), + 'is_block_theme' => $is_block_theme, + ); + + if ( $is_active ) { + $data['global_styles_id'] = $this->get_global_styles_id( $theme ); + } + + return $data; + } + + /** + * Ensure global styles post exists for a block theme. + * + * @param \WP_Theme $theme Theme object. + */ + private function ensure_global_styles_post_exists( $theme ): void { + $is_block_theme = is_callable( array( $theme, 'is_block_theme' ) ) ? $theme->is_block_theme() : false; + + if ( $is_block_theme && class_exists( 'WP_Theme_JSON_Resolver' ) ) { + \WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); + } + } + + /** + * Get the global styles post ID for a theme. + * + * @param \WP_Theme $theme Theme object. + * @return int Global styles post ID or 0. + */ + private function get_global_styles_id( $theme ): int { + $is_block_theme = is_callable( array( $theme, 'is_block_theme' ) ) ? $theme->is_block_theme() : false; + + if ( $is_block_theme && class_exists( 'WP_Theme_JSON_Resolver' ) ) { + return (int) \WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); + } + + return 0; + } +} diff --git a/includes/Mcp/Tools/Themes/SwitchTheme.php b/includes/Mcp/Tools/Themes/SwitchTheme.php new file mode 100644 index 0000000..5702c67 --- /dev/null +++ b/includes/Mcp/Tools/Themes/SwitchTheme.php @@ -0,0 +1,142 @@ + __( 'Switch Theme', 'airo-wp' ), + 'description' => __( 'Switches to an already-installed theme', 'airo-wp' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'theme_slug' => array( + 'type' => 'string', + 'description' => __( 'The theme slug (e.g. "twentytwentyfour")', 'airo-wp' ), + 'minLength' => 1, + ), + ), + 'required' => array( 'theme_slug' ), + ), + 'output_schema' => $this->build_output_schema( + __( 'Theme switch result', 'airo-wp' ), + array( + 'theme' => array( + 'type' => 'string', + 'description' => __( 'The theme slug', 'airo-wp' ), + ), + 'previous_theme' => array( + 'type' => 'string', + 'description' => __( 'The previously active theme slug', 'airo-wp' ), + ), + ) + ), + 'execute_callback' => array( $this, 'execute' ), + 'permission_callback' => array( $this, 'check_permissions' ), + 'category' => 'theme-management', + ) + ); + } + + /** + * Execute the tool. + * + * @param array $input Input parameters. + * @return array Execution result. + */ + public function execute( array $input ): array { + $slug = isset( $input['theme_slug'] ) ? sanitize_text_field( $input['theme_slug'] ) : ''; + + if ( empty( $slug ) ) { + return array( + 'success' => false, + 'message' => __( 'Theme slug is required.', 'airo-wp' ), + ); + } + + + $current_theme = get_stylesheet(); + + if ( $current_theme === $slug ) { + return array( + 'success' => true, + 'message' => __( 'Theme is already active.', 'airo-wp' ), + 'theme' => $slug, + 'previous_theme' => $current_theme, + ); + } + + $theme = wp_get_theme( $slug ); + + if ( ! $theme->exists() ) { + return array( + 'success' => false, + 'message' => sprintf( + /* translators: %s: theme slug */ + __( 'Theme "%s" is not installed. Use the activate-theme tool to install and activate it.', 'airo-wp' ), + $slug + ), + ); + } + + switch_theme( $slug ); + + if ( get_stylesheet() !== $slug ) { + return array( + 'success' => false, + 'message' => __( 'Theme switch failed.', 'airo-wp' ), + 'theme' => $slug, + ); + } + + return array( + 'success' => true, + 'message' => sprintf( + /* translators: 1: new theme name, 2: previous theme name */ + __( 'Successfully switched from "%2$s" to "%1$s".', 'airo-wp' ), + $theme->get( 'Name' ), + wp_get_theme( $current_theme )->get( 'Name' ) + ), + 'theme' => $slug, + 'previous_theme' => $current_theme, + ); + } +} diff --git a/includes/PackageInterface.php b/includes/PackageInterface.php new file mode 100644 index 0000000..72c4852 --- /dev/null +++ b/includes/PackageInterface.php @@ -0,0 +1,25 @@ +> + */ + private static function packages(): array { + return array( + RestPackage::class, + McpPackage::class, + BlocksPackage::class, + ); + } +} diff --git a/includes/Proxies/LegacyProxy.php b/includes/Proxies/LegacyProxy.php new file mode 100644 index 0000000..fe60312 --- /dev/null +++ b/includes/Proxies/LegacyProxy.php @@ -0,0 +1,29 @@ +service = $service; + } + + /** + * Register REST routes for publish and discard. + */ + public function register_routes(): void { + register_rest_route( + self::NAMESPACE, + '/drafts/(?P\d+)/publish', + array( + 'methods' => 'POST', + 'callback' => array( $this, 'publish_draft' ), + 'permission_callback' => array( $this, 'publish_permissions_check' ), + 'args' => array( + 'id' => array( + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + + register_rest_route( + self::NAMESPACE, + '/drafts/(?P\d+)/discard', + array( + 'methods' => 'DELETE', + 'callback' => array( $this, 'discard_draft' ), + 'permission_callback' => array( $this, 'discard_permissions_check' ), + 'args' => array( + 'id' => array( + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + } + + /** + * Publish a page draft. + * + * @param \WP_REST_Request $request REST request. + * @return \WP_REST_Response|\WP_Error Response on success, WP_Error on failure. + */ + public function publish_draft( \WP_REST_Request $request ) { + $draft_id = (int) $request->get_param( 'id' ); + $original_id = (int) get_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, true ); + + if ( empty( $original_id ) ) { + return new \WP_Error( + 'missing_original_link', + __( 'Draft is not linked to an original page.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + $result = $this->service->publish_draft( $draft_id ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + return new \WP_REST_Response( + array( + 'success' => true, + 'original_id' => $original_id, + 'redirect_url' => get_edit_post_link( $original_id, 'raw' ), + ), + 200 + ); + } + + /** + * Discard a page draft. + * + * @param \WP_REST_Request $request REST request. + * @return \WP_REST_Response|\WP_Error Response on success, WP_Error on failure. + */ + public function discard_draft( \WP_REST_Request $request ) { + $draft_id = (int) $request->get_param( 'id' ); + $original_id = (int) get_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, true ); + + $result = $this->service->discard_draft( $draft_id ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + $redirect_url = null; + + if ( ! empty( $original_id ) && get_post( $original_id ) ) { + $redirect_url = get_edit_post_link( $original_id, 'raw' ); + } + + return new \WP_REST_Response( + array( + 'success' => true, + 'original_id' => $original_id, + 'redirect_url' => $redirect_url, + ), + 200 + ); + } + + /** + * Check permissions for publishing a draft. + * + * @param \WP_REST_Request $request REST request. + * @return bool True if the user can publish this draft. + */ + public function publish_permissions_check( \WP_REST_Request $request ): bool { + $draft_id = (int) $request->get_param( 'id' ); + $original_id = (int) get_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, true ); + + if ( empty( $original_id ) ) { + return false; + } + + if ( ! current_user_can( 'edit_post', $original_id ) ) { + return false; + } + + return current_user_can( 'edit_post', $draft_id ); + } + + /** + * Check permissions for discarding a draft. + * + * @param \WP_REST_Request $request REST request. + * @return bool True if the user can discard this draft. + */ + public function discard_permissions_check( \WP_REST_Request $request ): bool { + $draft_id = (int) $request->get_param( 'id' ); + $original_id = (int) get_post_meta( $draft_id, DraftPageService::META_DRAFT_OF, true ); + + if ( ! empty( $original_id ) && ! current_user_can( 'edit_post', $original_id ) ) { + return false; + } + + return current_user_can( 'delete_post', $draft_id ); + } +} diff --git a/includes/Rest/Package.php b/includes/Rest/Package.php new file mode 100644 index 0000000..3d38e65 --- /dev/null +++ b/includes/Rest/Package.php @@ -0,0 +1,50 @@ +get( $endpoint_class ); + add_action( 'rest_api_init', array( $endpoint, 'register_routes' ) ); + } + } + + /** + * Registered REST endpoint classes. + * + * @return array + */ + private static function endpoints(): array { + return array( + DraftPages::class, + ); + } +} diff --git a/includes/Services/DraftPageService.php b/includes/Services/DraftPageService.php new file mode 100644 index 0000000..885c5c9 --- /dev/null +++ b/includes/Services/DraftPageService.php @@ -0,0 +1,369 @@ +post_status ) { + return null; + } + + $draft_of = get_post_meta( $draft->ID, self::META_DRAFT_OF, true ); + + if ( (int) $draft_of !== $post_id ) { + return null; + } + + return $draft; + } + + /** + * Check whether a published page has a draft. + * + * @param int $post_id The published page ID. + * @return bool True if a valid draft exists. + */ + public function has_draft( int $post_id ): bool { + return null !== $this->get_draft( $post_id ); + } + + /** + * Publish a draft by copying its content to the original and cleaning up. + * + * @param int $draft_id The draft post ID. + * @param bool $force_delete Whether to permanently delete (true) or move to trash (false). + * @return true|\WP_Error True on success, WP_Error on failure. + */ + public function publish_draft( int $draft_id, bool $force_delete = false ) { + $draft = get_post( $draft_id ); + + if ( ! $draft ) { + return new \WP_Error( + 'draft_not_found', + __( 'Draft post not found.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + if ( 'page' !== $draft->post_type ) { + return new \WP_Error( + 'not_a_page', + __( 'Post is not a page.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + if ( 'draft' !== $draft->post_status ) { + return new \WP_Error( + 'not_a_draft', + __( 'Post is not a draft.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + $original_id = get_post_meta( $draft_id, self::META_DRAFT_OF, true ); + + if ( empty( $original_id ) ) { + return new \WP_Error( + 'missing_original_link', + __( 'Draft is not linked to an original page.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + $original = get_post( (int) $original_id ); + + if ( ! $original ) { + return new \WP_Error( + 'original_not_found', + __( 'Original page not found.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + if ( 'publish' !== $original->post_status ) { + return new \WP_Error( + 'original_not_published', + __( 'Original page is not published.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + $update_result = wp_update_post( + array( + 'ID' => $original->ID, + 'post_content' => $draft->post_content, + 'post_title' => $draft->post_title, + 'post_excerpt' => $draft->post_excerpt, + 'menu_order' => $draft->menu_order, + 'post_password' => $draft->post_password, + 'comment_status' => $draft->comment_status, + 'ping_status' => $draft->ping_status, + 'post_content_filtered' => $draft->post_content_filtered, + ), + true + ); + + if ( is_wp_error( $update_result ) ) { + return $update_result; + } + + // Handle thumbnail: copy from draft or delete from original. + $draft_thumbnail = get_post_meta( $draft_id, '_thumbnail_id', true ); + + if ( ! empty( $draft_thumbnail ) ) { + update_post_meta( $original->ID, '_thumbnail_id', $draft_thumbnail ); + } else { + delete_post_meta( $original->ID, '_thumbnail_id' ); + } + + $this->replace_post_meta( $draft_id, $original->ID ); + + // Clean up meta links — always remove the "has draft" pointer from the original. + delete_post_meta( $original->ID, self::META_HAS_DRAFT ); + + if ( $force_delete ) { + delete_post_meta( $draft_id, self::META_DRAFT_OF ); + delete_post_meta( $draft_id, self::META_DRAFT_CREATED ); + wp_delete_post( $draft_id, true ); + } else { + // Preserve META_DRAFT_OF and META_DRAFT_CREATED so a trashed draft can be restored. + $trashed = wp_trash_post( $draft_id ); + + if ( ! $trashed ) { + return new \WP_Error( + 'trash_failed', + __( 'Failed to move draft to trash.', 'airo-wp' ), + array( 'status' => 500 ) + ); + } + } + + /** + * Fires after a page draft has been published. + * + * @param int $original_id The original page ID. + * @param int $draft_id The draft post ID that was published. + */ + do_action( 'airowp_draft_published', $original->ID, $draft_id ); + + return true; + } + + /** + * Discard a draft by deleting it and cleaning up meta. + * + * @param int $draft_id The draft post ID. + * @param bool $force_delete Whether to permanently delete (true) or move to trash (false). + * @return true|\WP_Error True on success, WP_Error on failure. + */ + public function discard_draft( int $draft_id, bool $force_delete = false ) { + $draft = get_post( $draft_id ); + + if ( ! $draft ) { + return new \WP_Error( + 'draft_not_found', + __( 'Draft post not found.', 'airo-wp' ), + array( 'status' => 404 ) + ); + } + + if ( 'page' !== $draft->post_type ) { + return new \WP_Error( + 'not_a_page', + __( 'Post is not a page.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + if ( 'draft' !== $draft->post_status ) { + return new \WP_Error( + 'not_a_draft', + __( 'Post is not a draft.', 'airo-wp' ), + array( 'status' => 400 ) + ); + } + + $original_id = get_post_meta( $draft_id, self::META_DRAFT_OF, true ); + + if ( ! empty( $original_id ) ) { + delete_post_meta( (int) $original_id, self::META_HAS_DRAFT ); + } + + if ( $force_delete ) { + delete_post_meta( $draft_id, self::META_DRAFT_OF ); + delete_post_meta( $draft_id, self::META_DRAFT_CREATED ); + wp_delete_post( $draft_id, true ); + } else { + // Preserve META_DRAFT_OF and META_DRAFT_CREATED so a trashed draft can be restored. + $trashed = wp_trash_post( $draft_id ); + + if ( ! $trashed ) { + return new \WP_Error( + 'trash_failed', + __( 'Failed to move draft to trash.', 'airo-wp' ), + array( 'status' => 500 ) + ); + } + } + + /** + * Fires after a page draft has been discarded. + * + * @param int $draft_id The draft post ID that was discarded. + * @param int $original_id The original page ID (0 if no link existed). + */ + do_action( 'airowp_draft_discarded', $draft_id, ! empty( $original_id ) ? (int) $original_id : 0 ); + + return true; + } + + /** + * Copy post meta from one post to another, excluding internal keys. + * + * @param int $source_id Source post ID. + * @param int $target_id Target post ID. + */ + public function copy_post_meta( int $source_id, int $target_id ): void { + $meta = get_post_meta( $source_id ); + $excluded = $this->get_draft_excluded_meta_keys( $source_id ); + + if ( ! is_array( $meta ) ) { + return; + } + + foreach ( $meta as $key => $values ) { + if ( in_array( $key, $excluded, true ) ) { + continue; + } + + foreach ( $values as $value ) { + // Skip object values that cannot be reliably serialized. + if ( is_object( $value ) ) { + continue; + } + + add_post_meta( $target_id, $key, maybe_unserialize( $value ) ); + } + } + } + + /** + * Hook for `before_delete_post`. Cleans up draft meta relationships. + * + * @param int $post_id The post being deleted. + */ + public function cleanup_draft_meta( int $post_id ): void { + // If this post is a draft, clean parent's META_HAS_DRAFT. + $original_id = get_post_meta( $post_id, self::META_DRAFT_OF, true ); + + if ( ! empty( $original_id ) ) { + delete_post_meta( (int) $original_id, self::META_HAS_DRAFT ); + } + + // If this post has a draft linked to it, delete that draft. + $draft_id = get_post_meta( $post_id, self::META_HAS_DRAFT, true ); + + if ( ! empty( $draft_id ) ) { + wp_delete_post( (int) $draft_id, true ); + } + } + + /** + * Replace all non-excluded meta on the target with meta from the source. + * + * @param int $source_id Source post ID. + * @param int $target_id Target post ID. + */ + private function replace_post_meta( int $source_id, int $target_id ): void { + $source_meta = get_post_meta( $source_id ); + $excluded = $this->get_draft_excluded_meta_keys( $source_id ); + + if ( ! is_array( $source_meta ) ) { + return; + } + + // Delete target meta that exists on the source (non-excluded). + foreach ( array_keys( $source_meta ) as $key ) { + if ( in_array( $key, $excluded, true ) ) { + continue; + } + + delete_post_meta( $target_id, $key ); + } + + $this->copy_post_meta( $source_id, $target_id ); + } + + /** + * Get meta keys excluded from draft copy operations. + * + * @param int $source_id Source post ID for filter context. + * @return array Excluded meta keys. + */ + private function get_draft_excluded_meta_keys( int $source_id ): array { + $excluded_keys = array( + '_edit_lock', + '_edit_last', + self::META_DRAFT_OF, + self::META_HAS_DRAFT, + self::META_DRAFT_CREATED, + '_wp_old_slug', + '_wp_old_date', + ); + + /** + * Filters the meta keys excluded from draft copy operations. + * + * @param array $excluded_keys Default excluded meta keys. + * @param int $source_id Source post ID. + */ + return apply_filters( 'airowp_draft_excluded_meta_keys', $excluded_keys, $source_id ); + } +} diff --git a/includes/Services/FontDownloader.php b/includes/Services/FontDownloader.php new file mode 100644 index 0000000..dbafcac --- /dev/null +++ b/includes/Services/FontDownloader.php @@ -0,0 +1,339 @@ +is_google_fonts_url( $src_url ) ) { + continue; + } + + // Download the font and get local path. + $local_path = $this->download_google_font( + $src_url, + $font_slug, + $font_face['fontWeight'] ?? '400', + $font_face['fontStyle'] ?? 'normal' + ); + + if ( $local_path ) { + // Update src to use local file path. + $font_face['src'] = array( $local_path ); + } else { + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + sprintf( + '[Font Downloader] Failed to download %s (%s %s) from %s', + $font_name, + $font_face['fontWeight'] ?? '400', + $font_face['fontStyle'] ?? 'normal', + $src_url + ) + ); + } + } + } + + return $font_families; + } + + /** + * Check if URL is a Google Fonts URL. + * + * @param string $url URL to check. + * @return bool True if Google Fonts URL. + */ + private function is_google_fonts_url( string $url ): bool { + return strpos( $url, 'fonts.googleapis.com' ) !== false; + } + + /** + * Download a Google Font and return local file path. + * + * @param string $google_fonts_css_url Google Fonts CSS URL. + * @param string $font_slug Font slug for filename. + * @param string $weight Font weight. + * @param string $style Font style. + * @return string|null Local file path or null on failure. + */ + private function download_google_font( string $google_fonts_css_url, string $font_slug, string $weight, string $style ): ?string { + // Parse Google Fonts CSS to get actual font file URL. + $font_file_url = $this->parse_google_fonts_css( $google_fonts_css_url ); + + if ( ! $font_file_url ) { + error_log( '[Font Downloader] Failed to parse Google Fonts CSS: ' . $google_fonts_css_url ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + return null; + } + + // Generate filename. + $filename = $this->generate_filename( $font_slug, $weight, $style, $font_file_url ); + + // Get WordPress fonts directory. + $font_dir = wp_font_dir(); + if ( ! $font_dir || ! isset( $font_dir['path'] ) ) { + error_log( '[Font Downloader] Could not get WordPress font directory' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + return null; + } + + $font_dir_path = $font_dir['path']; + $font_file_path = trailingslashit( $font_dir_path ) . $filename; + + // Check if font already exists. + if ( file_exists( $font_file_path ) ) { + // Return relative path using file:./ prefix (WordPress standard for portability). + // This path is relative to theme directory and resolved dynamically on page load. + return 'file:./../../uploads/fonts/' . $filename; + } + + // Download the font file with retry logic. + $downloaded_file = $this->download_with_retry( $font_file_url ); + + if ( ! $downloaded_file ) { + return null; + } + + // Ensure fonts directory exists. + if ( ! file_exists( $font_dir_path ) ) { + wp_mkdir_p( $font_dir_path ); + } + + // Move downloaded file to fonts directory using WP_Filesystem. + if ( ! function_exists( 'WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + WP_Filesystem(); + global $wp_filesystem; + + $moved = $wp_filesystem->move( $downloaded_file, $font_file_path, true ); + + if ( ! $moved ) { + error_log( '[Font Downloader] Failed to move font file to: ' . $font_file_path ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + $wp_filesystem->delete( $downloaded_file ); + return null; + } + + // Validate the downloaded file. + if ( ! $this->validate_font_file( $font_file_path ) ) { + error_log( '[Font Downloader] Font file validation failed: ' . $filename ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + $wp_filesystem->delete( $font_file_path ); + return null; + } + + // Return relative path using file:./ prefix (WordPress standard for portability). + // This path is relative to theme directory and resolved dynamically on page load. + return 'file:./../../uploads/fonts/' . $filename; + } + + /** + * Parse Google Fonts CSS to extract actual font file URL. + * + * @param string $css_url Google Fonts CSS URL. + * @return string|null Font file URL or null on failure. + */ + private function parse_google_fonts_css( string $css_url ): ?string { + // Fetch the CSS content. + $response = wp_remote_get( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + $css_url, + array( + 'timeout' => 3, + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + ) + ); + + if ( is_wp_error( $response ) ) { + error_log( '[Font Downloader] Failed to fetch Google Fonts CSS: ' . $response->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + return null; + } + + $css_content = wp_remote_retrieve_body( $response ); + + if ( empty( $css_content ) ) { + return null; + } + + // Extract font file URL from CSS. + // Google Fonts returns multiple @font-face rules with unicode-range subsetting. + // We want the Latin subset (U+0000-00FF) which covers English and basic Latin characters. + // This is typically the last @font-face rule in the response. + + // Try to find the Latin subset specifically (look for comment or unicode-range). + if ( preg_match( '/\/\*\s*latin\s*\*\/.*?url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/s', $css_content, $matches ) ) { + return $matches[1]; + } + + // Fallback: Get all woff2 URLs and return the last one (usually Latin subset). + if ( preg_match_all( '/url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/', $css_content, $matches ) ) { + // Return the last match (Latin subset is typically last). + return end( $matches[1] ); + } + + // Final fallback: try to find any woff2 URL. + if ( preg_match( '/url\((https:\/\/[^)]+\.woff2)\)/', $css_content, $matches ) ) { + return $matches[1]; + } + + return null; + } + + /** + * Download file with retry logic and exponential backoff. + * + * @param string $url URL to download. + * @return string|null Path to downloaded file or null on failure. + */ + private function download_with_retry( string $url ): ?string { + $attempt = 0; + $delay = self::INITIAL_RETRY_DELAY; + + while ( $attempt < self::MAX_ATTEMPTS ) { + ++$attempt; + + $temp_file = download_url( $url, 30 ); // 30 second timeout. + + if ( ! is_wp_error( $temp_file ) ) { + return $temp_file; + } + + $error_message = $temp_file->get_error_message(); + + // Don't retry if it's a 404 or similar permanent error. + if ( strpos( $error_message, '404' ) !== false || strpos( $error_message, '403' ) !== false ) { + return null; + } + + if ( $attempt < self::MAX_ATTEMPTS ) { + sleep( $delay ); // phpcs:ignore WordPress.WP.AlternativeFunctions.sleep_sleep + $delay *= 2; // Exponential backoff. + } + } + + return null; + } + + /** + * Generate filename for font file. + * + * @param string $font_slug Font slug. + * @param string $weight Font weight. + * @param string $style Font style. + * @param string $url Font file URL (to extract extension). + * @return string Filename. + */ + private function generate_filename( string $font_slug, string $weight, string $style, string $url ): string { + // Extract extension from URL. + $extension = 'woff2'; // Default. + if ( preg_match( '/\.([a-z0-9]+)(\?|$)/i', $url, $matches ) ) { + $extension = $matches[1]; + } + + // Sanitize components. + $font_slug = sanitize_file_name( $font_slug ); + $weight = sanitize_file_name( $weight ); + $style = sanitize_file_name( $style ); + + return sprintf( '%s-%s-%s.%s', $font_slug, $weight, $style, $extension ); + } + + /** + * Validate that the downloaded file is a valid font file. + * + * @param string $file_path Path to font file. + * @return bool True if valid. + */ + private function validate_font_file( string $file_path ): bool { + // Check file exists and is readable. + if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) { + return false; + } + + // Check file size (fonts should be at least 1KB, max 5MB). + $file_size = filesize( $file_path ); + if ( $file_size < 1024 || $file_size > 5 * 1024 * 1024 ) { + return false; + } + + // Check file signature for WOFF2 format. + $handle = fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + if ( ! $handle ) { + return false; + } + + $signature = fread( $handle, 4 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread + fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose + + // WOFF2 signature: "wOF2" (0x774F4632). + if ( 'wOF2' === $signature ) { + return true; + } + + // WOFF signature: "wOFF" (0x774F4646). + if ( 'wOFF' === $signature ) { + return true; + } + + // TTF/OTF signatures. + $first_bytes = unpack( 'N', $signature ); + if ( $first_bytes && in_array( $first_bytes[1], array( 0x00010000, 0x4F54544F ), true ) ) { + return true; + } + + return false; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b13fccd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24462 @@ +{ + "name": "airo-wp", + "version": "0.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "airo-wp", + "version": "0.2.4", + "devDependencies": { + "@playwright/test": "^1.52.0", + "@wordpress/e2e-test-utils-playwright": "^1.50.0", + "@wordpress/icons": "^10.0.0", + "@wordpress/scripts": "^30.0.0", + "@wp-playground/cli": "3.1.43", + "classnames": "^2.3.2", + "countup.js": "^2.9.0", + "glob": "^11.0.0", + "leaflet": "^1.9.4", + "typescript": "^5.4.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "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/eslint-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.7.tgz", + "integrity": "sha512-B+BO9x86VYsQHimucBAL1fxTJKF4wyKY6ZVzee9QgzdZOUfs3BaR6AQrgoGrRI+7IFS1wUz/VyQ+SoBcSpdPbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "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, + "license": "MIT", + "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-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^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-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.29.7.tgz", + "integrity": "sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.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, + "license": "MIT", + "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.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "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": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", + "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/JounQin" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "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, + "license": "Apache-2.0", + "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, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.7.tgz", + "integrity": "sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/reporters/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/app": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", + "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.4.tgz", + "integrity": "sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-app/node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "18 >=18.20 || 20 || >=22" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", + "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.3.2.tgz", + "integrity": "sha512-exj1MzVXoP7xnAcAB3jZ97pTvVPkQF9y6GA/dvYC47HV7vLv+24XRS6b/v/XnyikpEuvMhugEXdGtAlU086WkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.6.1", + "aggregate-error": "^3.1.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", + "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.57.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", + "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.1.tgz", + "integrity": "sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.1.tgz", + "integrity": "sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.1.tgz", + "integrity": "sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz", + "integrity": "sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.1.tgz", + "integrity": "sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.1.tgz", + "integrity": "sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.2.tgz", + "integrity": "sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.2.tgz", + "integrity": "sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/instrumentation": "0.57.2", + "@opentelemetry/semantic-conventions": "1.28.0", + "forwarded-parse": "2.1.2", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-http/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.1.tgz", + "integrity": "sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.1.tgz", + "integrity": "sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.1.tgz", + "integrity": "sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.1.tgz", + "integrity": "sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.1.tgz", + "integrity": "sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.52.0.tgz", + "integrity": "sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.1.tgz", + "integrity": "sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.1.tgz", + "integrity": "sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.2.tgz", + "integrity": "sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.51.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.1.tgz", + "integrity": "sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1", + "@types/pg": "8.6.1", + "@types/pg-pool": "2.0.6" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis-4": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.1.tgz", + "integrity": "sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.1.tgz", + "integrity": "sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.1.tgz", + "integrity": "sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/instrumentation/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", + "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", + "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.59.tgz", + "integrity": "sha512-439NUzQGmH+9Y017/xCchBP9571J4bzhpcNhrxorf7r37wcyJZkgUfrUsRL3xl+JDcZ6ORhoFCzCw98c6S3YHw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "legacy-javascript": "latest", + "third-party-web": "latest" + } + }, + "node_modules/@php-wasm/cli-util": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/cli-util/-/cli-util-3.1.43.tgz", + "integrity": "sha512-O6w8ggFB8Jq0vUU0cVoW2Dr3ICjfjAx7uW0Y4W31DuzTBsdQojuIMbmNIoUhOTGBIdswM3THRIPE7FsMIdmnFA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/util": "3.1.43", + "fast-xml-parser": "^5.8.0", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/logger": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/logger/-/logger-3.1.43.tgz", + "integrity": "sha512-A/48AT49l9HsGBUJoopTfV80HDKZ/3Tp4nZlI1M5qY4tdD/VkCWiAys5NwCeU0Kr8mvTsrH4B0HCLnJvyHSFLw==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node/-/node-3.1.43.tgz", + "integrity": "sha512-KbyjWs1PLdc4pdDSaI+wp5DjVBADIps1Vzg+MtX3z5qVaqrDXKaigI1lt0P6j/MJ1wHD3oiEaekPwv2ch5hbeg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/cli-util": "3.1.43", + "@php-wasm/logger": "3.1.43", + "@php-wasm/node-5-2": "3.1.43", + "@php-wasm/node-7-4": "3.1.43", + "@php-wasm/node-8-0": "3.1.43", + "@php-wasm/node-8-1": "3.1.43", + "@php-wasm/node-8-2": "3.1.43", + "@php-wasm/node-8-3": "3.1.43", + "@php-wasm/node-8-4": "3.1.43", + "@php-wasm/node-8-5": "3.1.43", + "@php-wasm/universal": "3.1.43", + "@php-wasm/util": "3.1.43", + "fs-ext-extra-prebuilt": "2.2.7", + "wasm-feature-detect": "1.8.0", + "ws": "8.21.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-5-2": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-5-2/-/node-5-2-3.1.43.tgz", + "integrity": "sha512-F+uoeiA36WaxT96eIepQEC4ObEP2/l2x46AiNBW2B0JahD8O48uh9CoA2ZPCm1WVa/VBJmK7LPXxJMxSWvTNPQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-7-4": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-7-4/-/node-7-4-3.1.43.tgz", + "integrity": "sha512-3SZvBKnnVQYMnOBE3S1mDv/OamWaQMUJeX/EnSLW8ZYaPRV+ZhJ2zRPK0JQaUrw0IBcqDsOW15vRW0GDPvW0cg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-0": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-0/-/node-8-0-3.1.43.tgz", + "integrity": "sha512-6plvqUhd/ehOKCLuazXvL0vD6YBrYa28EQf0xGjY+N+EB7H84QeCSBYiOMpn75snS6a2cGbFvjLPQ09guZOO+w==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-1": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-1/-/node-8-1-3.1.43.tgz", + "integrity": "sha512-FfXc9aq39VJSEneTbWCtENla8rqchH7C/FGsNhA72R+v6CcTyjYeSE8R3Ua+WbaZc+GzqKUNjKNaouJcdc9wPg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-2": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-2/-/node-8-2-3.1.43.tgz", + "integrity": "sha512-Mc6tNXCXPVBnhuwkxtimzIBQpQUbFMY78M1mKzXTOYHZZ7qiNUiQjGUyifs1T5qMnKTgauVcsWDPI1imR5GkCw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-3": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-3/-/node-8-3-3.1.43.tgz", + "integrity": "sha512-ZxXo/ndnym0kAXZD/gQX9qFQUyM0Ka6i9TvlZuPLxO19imqS+pIfF3kO/a73FzROUyKb0+RGJdX+Fd6u0fkURA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-4": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-4/-/node-8-4-3.1.43.tgz", + "integrity": "sha512-T5d+ZKZHKortmBdFI6bJGq6hsRLtn5el2AfKDppoYgNKBnOY2Cx1uI/oVgMSaBd8TsNq2SpLFrx72QRJ0xVbzQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/node-8-5": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/node-8-5/-/node-8-5-3.1.43.tgz", + "integrity": "sha512-suiDZUn8Cqm+52AdlTQWGcwLhBEmWE541Ip5EMzTIFidolvH/7JS3+nR7RYQ9FMykEkV7g4kiVzTDXPaZ+iA5w==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "wasm-feature-detect": "1.8.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/progress": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/progress/-/progress-3.1.43.tgz", + "integrity": "sha512-8obpwqbSgFywWs699YdMsKKMVqiZ9evpSKpf1T4yMdAOHmFkJeOWdnkYlOe0MXe9F6U0Ui0EBmsi67G7MNplxw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.43" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/scopes": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/scopes/-/scopes-3.1.43.tgz", + "integrity": "sha512-3nFmuz9W+Ymb2LvTM/Gi2PiR7E8NeTr0MIE2Qh8OtSv2hWFa/uo5g6B5kB422AfeRr29CEk8AndoIVHweeM8NA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/stream-compression": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/stream-compression/-/stream-compression-3.1.43.tgz", + "integrity": "sha512-LvForUJZ+0IRepTbEENClA66XIx9Vq433eGyDwuyBLRsAyH5Z4AnF4tIcYjCrrCp4P3yvimXlmYjsliJsEerbw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/util": "3.1.43" + } + }, + "node_modules/@php-wasm/universal": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/universal/-/universal-3.1.43.tgz", + "integrity": "sha512-bWq5MC2Uflx2gYpS+M4CHhNpY4pImtabRYapCzzpHvjvmKlnmisc38jSMErkwXSquNjc3MuFcEahXoxMkmvjNA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.43", + "@php-wasm/progress": "3.1.43", + "@php-wasm/stream-compression": "3.1.43", + "@php-wasm/util": "3.1.43", + "ini": "4.1.2" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/util": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/util/-/util-3.1.43.tgz", + "integrity": "sha512-Eb/M3u8FCT8yH5Wgm+JQ7ydhekRex6Mj6HAFu1pxozI+rZC8yjv91Wo9bLOZ1kjoyipReHXMyhRrEZwdmWUwoQ==", + "dev": true, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/web-service-worker": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/web-service-worker/-/web-service-worker-3.1.43.tgz", + "integrity": "sha512-dsDulJDsjBAWjJ9X8ADrdrHJBfBM55fRqh39/G2vSOOHqc1H5cupGI4rW/PqBvoxieZ3hQCusdLQan8lPVMG+w==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/scopes": "3.1.43", + "@php-wasm/universal": "3.1.43" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@php-wasm/xdebug-bridge": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@php-wasm/xdebug-bridge/-/xdebug-bridge-3.1.43.tgz", + "integrity": "sha512-Np0RkX/kHgKdlAYzXeCuV5JGXDdkoDM5mTF5P60PdXcjIsBlm/Z44Aph8DufTZI89SAQTEcOF99JMJFAoX2O/g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.43", + "@php-wasm/universal": "3.1.43", + "ws": "8.21.0", + "xml2js": "0.6.2", + "yargs": "17.7.2" + }, + "bin": { + "xdebug-bridge": "xdebug-bridge.js" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", + "integrity": "sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prisma/instrumentation": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.11.1.tgz", + "integrity": "sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sentry/core": { + "version": "9.47.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.47.1.tgz", + "integrity": "sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "9.47.1", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.47.1.tgz", + "integrity": "sha512-CDbkasBz3fnWRKSFs6mmaRepM2pa+tbZkrqhPWifFfIkJDidtVW40p6OnquTvPXyPAszCnDZRnZT14xyvNmKPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.2", + "@opentelemetry/instrumentation-amqplib": "^0.46.1", + "@opentelemetry/instrumentation-connect": "0.43.1", + "@opentelemetry/instrumentation-dataloader": "0.16.1", + "@opentelemetry/instrumentation-express": "0.47.1", + "@opentelemetry/instrumentation-fs": "0.19.1", + "@opentelemetry/instrumentation-generic-pool": "0.43.1", + "@opentelemetry/instrumentation-graphql": "0.47.1", + "@opentelemetry/instrumentation-hapi": "0.45.2", + "@opentelemetry/instrumentation-http": "0.57.2", + "@opentelemetry/instrumentation-ioredis": "0.47.1", + "@opentelemetry/instrumentation-kafkajs": "0.7.1", + "@opentelemetry/instrumentation-knex": "0.44.1", + "@opentelemetry/instrumentation-koa": "0.47.1", + "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", + "@opentelemetry/instrumentation-mongodb": "0.52.0", + "@opentelemetry/instrumentation-mongoose": "0.46.1", + "@opentelemetry/instrumentation-mysql": "0.45.1", + "@opentelemetry/instrumentation-mysql2": "0.45.2", + "@opentelemetry/instrumentation-pg": "0.51.1", + "@opentelemetry/instrumentation-redis-4": "0.46.1", + "@opentelemetry/instrumentation-tedious": "0.18.1", + "@opentelemetry/instrumentation-undici": "0.10.1", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@prisma/instrumentation": "6.11.1", + "@sentry/core": "9.47.1", + "@sentry/node-core": "9.47.1", + "@sentry/opentelemetry": "9.47.1", + "import-in-the-middle": "^1.14.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "9.47.1", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.47.1.tgz", + "integrity": "sha512-7TEOiCGkyShJ8CKtsri9lbgMCbB+qNts2Xq37itiMPN2m+lIukK3OX//L8DC5nfKYZlgikrefS63/vJtm669hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "9.47.1", + "@sentry/opentelemetry": "9.47.1", + "import-in-the-middle": "^1.14.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/resources": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "9.47.1", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.47.1.tgz", + "integrity": "sha512-STtFpjF7lwzeoedDJV+5XA6P89BfmFwFftmHSGSe3UTI8z8IoiR5yB6X2vCjSPvXlfeOs13qCNNCEZyznxM8Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/core": "9.47.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@stylistic/stylelint-plugin": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.3.tgz", + "integrity": "sha512-85fsmzgsIVmyG3/GFrjuYj6Cz8rAM7IZiPiXCMiSMfoDOC1lOrzrXPDk24WqviAghnPqGpx8b0caK2PuewWGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1", + "@csstools/media-query-list-parser": "^3.0.1", + "is-plain-object": "^5.0.0", + "postcss": "^8.4.41", + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0", + "style-search": "^0.1.0" + }, + "engines": { + "node": "^18.12 || >=20.9" + }, + "peerDependencies": { + "stylelint": "^16.8.0" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@tannin/compile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", + "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tannin/evaluate": "^1.2.0", + "@tannin/postfix": "^1.1.0" + } + }, + "node_modules/@tannin/evaluate": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", + "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tannin/plural-forms": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", + "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tannin/compile": "^1.1.0" + } + }, + "node_modules/@tannin/postfix": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", + "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tannin/sprintf": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@tannin/sprintf/-/sprintf-1.3.3.tgz", + "integrity": "sha512-RwARl+hFwhzy0tg9atWcchLFvoQiOh4rrP7uG2N5E4W80BPCUX0ElcUR9St43fxB9EfjsW2df9Qp+UsTbvQDjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.162", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.162.tgz", + "integrity": "sha512-Fn658grtLOci1oxi1391vvDWJRKNGWRSqfxRkmN/Iy3c0tQH1USMKEXcPYHLvope+ZgTFocx9FRQJx1muBL6qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mousetrap": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.15.tgz", + "integrity": "sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/pg": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@wordpress/babel-preset-default": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.50.0.tgz", + "integrity": "sha512-nXF+cu0NA9lk4GPO+/iv3mMt1TV9zzjMalfaNPfafWz5VDGW68h82Le8wqclTewex/99kYWl12kHwDIx66hw6Q==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/core": "^7.25.7", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-transform-react-jsx": "^7.25.7", + "@babel/plugin-transform-runtime": "^7.25.7", + "@babel/preset-env": "^7.25.7", + "@babel/preset-typescript": "^7.25.7", + "@wordpress/browserslist-config": "^6.50.0", + "@wordpress/warning": "^3.50.0", + "browserslist": "^4.21.10", + "core-js": "^3.31.0", + "react": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/base-styles": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-10.2.0.tgz", + "integrity": "sha512-iDPbyyeUnxLFq4ec+03x87jT8lRoBzK2rDoEg24l2KNAR9ZJ2kwI0z2E/wW0fOZC37A7qr377KJslPHKLtMgyQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/browserslist-config": { + "version": "6.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.50.0.tgz", + "integrity": "sha512-/5Zn/uIvVw37iLJRa/HhqHQ/DJq4KWjEXTkFQ/NpCNFQm6ll54uDgYZ9f0tWQM7ORCzCU32z/IJ8ujZaMLA9nQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/compose": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.3.0.tgz", + "integrity": "sha512-nHrV8mLanqnLO67l+6RkotG9eRgiW1D6uVb919z8wVWwoZVfimFyN1nznH92tdE/xW6SnX37XDoLoLcS9IkwhQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/mousetrap": "^1.6.8", + "@wordpress/deprecated": "^4.50.0", + "@wordpress/dom": "^4.50.0", + "@wordpress/element": "^8.2.0", + "@wordpress/is-shallow-equal": "^5.50.0", + "@wordpress/keycodes": "^4.50.0", + "@wordpress/priority-queue": "^3.50.0", + "@wordpress/private-apis": "^1.50.0", + "@wordpress/undo-manager": "^1.50.0", + "change-case": "^4.1.2", + "mousetrap": "^1.6.5", + "use-memo-one": "^1.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18.3.27", + "react": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/compose/node_modules/@wordpress/element": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.2.0.tgz", + "integrity": "sha512-cTnKRHUdZbYAd4RaJiUwhDKdQDDWY7VoGiwektCmhiBQF6WTFYlhsxl/dmHE0Sok3q7PJQwAg538XEb8WuTaew==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/deprecated": "^4.50.0", + "@wordpress/escape-html": "^3.50.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/dependency-extraction-webpack-plugin": { + "version": "6.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.50.0.tgz", + "integrity": "sha512-YtYtLkAUdLXTCFwLxB/RbpBb19egvOvrEk/aZWxSXBT+sg6fNvTgYvkRMOqaVjja0CWDHGm9DJosZOnvAxuCDQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "json2php": "^0.0.9" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@wordpress/deprecated": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.50.0.tgz", + "integrity": "sha512-TVJavTbunFoCSo2g64EKtGwmDVJhvlSqsc4T1EM4aBwsYGIJPjHcAzh6KS8Z171QXFnBxZ7We8CCDAvOFU9iaA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/hooks": "^4.50.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/dom": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.50.0.tgz", + "integrity": "sha512-2Mognc1rwe1+tlIfbSeZPqbuAh6vJagELzw9cLasObiVayWhaG+LCIp0sJz2pPG9ppz8L31gGUgna7zrSRu85Q==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/deprecated": "^4.50.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/e2e-test-utils-playwright": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.50.0.tgz", + "integrity": "sha512-P55NxY86M8lNnAbEPmpEswbxBoB7BWVhg67KOLRsGwLMeJ4qHbBngmgBIUt72IAH3H2UWVq7jZ1Xm/J1ENOMoQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "change-case": "^4.1.2", + "get-port": "^5.1.1", + "lighthouse": "^12.2.2", + "mime": "^3.0.0", + "web-vitals": "^4.2.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": ">=1", + "@types/node": "^20.17.10" + } + }, + "node_modules/@wordpress/element": { + "version": "6.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.46.0.tgz", + "integrity": "sha512-hjnrqZi0cZVdkmN0xQavKfSQJYAkb9pVSnDPpuX65OLxeD9/EWkIXvFzBb+nH8c4NzKKSqQU96XCTQrH37OCIA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/escape-html": "^3.46.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/escape-html": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.50.0.tgz", + "integrity": "sha512-EECt++WB1RNPQBwBS7/6rKO4eUNq2xe6nb6Guv6uT+F74DgtM3+aZtNs7JhKFxRMORPtu7T0LhlAOBa4H5CsPQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/eslint-plugin": { + "version": "22.22.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.22.0.tgz", + "integrity": "sha512-DLGm5i8Gn0vjkZGKF49U2pYME5Jl9AvmoMJB2G508d+sB/oTSkPmM0baUP7G5zxbd1aqfNTaD0KjdyGyWFFKOA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/eslint-parser": "7.25.7", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", + "@wordpress/babel-preset-default": "^8.36.0", + "@wordpress/prettier-config": "^4.36.0", + "cosmiconfig": "^7.0.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jest": "^27.4.3", + "eslint-plugin-jsdoc": "^46.4.6", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-playwright": "^0.15.3", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react-hooks": "^4.3.0", + "globals": "^13.12.0", + "requireindex": "^1.2.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@babel/core": ">=7", + "eslint": ">=8", + "prettier": ">=3", + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/eslint-plugin/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@wordpress/hooks": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.50.0.tgz", + "integrity": "sha512-dV/AeP9iH1UydVWoUoCPuM8FUx2XvL1i65dhiwRHmZxK/zz8nR5Rq75WISpwFzRYfsyRThjl4LwUxhFAQnHrDw==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/i18n": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.23.0.tgz", + "integrity": "sha512-avuywVdBpOeHm9cghFcNXCKi/VlJhbJ6KN6LydQOy/NqdRbqyRc59/XlKk1Ks+Vb+dWTGKr5gjiSzm9QJETGmA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@tannin/sprintf": "^1.3.2", + "@wordpress/hooks": "^4.50.0", + "gettext-parser": "^1.3.1", + "memize": "^2.1.0", + "tannin": "^1.2.0" + }, + "bin": { + "pot-to-php": "tools/pot-to-php.js" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/icons": { + "version": "10.32.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.32.0.tgz", + "integrity": "sha512-1WvJdT361X1LnetYBpBWUjAVXZzl+pBdIwHbYRAp8ej47EI/igPmNxmq81nFd40s8fer/9qtipielcqSI6H2rA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "7.25.7", + "@wordpress/element": "^6.32.0", + "@wordpress/primitives": "^4.32.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/is-shallow-equal": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.50.0.tgz", + "integrity": "sha512-Cq1brW/OhGVNnCqSztHgmKQ1DsNUtEq4F1xIo1awBKVmXDWFggJRxzaWZHWC+sDZmBp7pu03jWqq2u/s+hazSQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/jest-console": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.50.0.tgz", + "integrity": "sha512-iV6h5nWylCrL/4Wt7kXvq8YRwhryLgxThliaPLVvMfHb4CM6MJzapoIw5l8YIs1CDRlBU6N9j9oAxLBXvhrHqA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "jest-matcher-utils": "^29.6.2", + "jest-mock": "^29.6.2" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "jest": ">=29" + } + }, + "node_modules/@wordpress/jest-preset-default": { + "version": "12.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.50.0.tgz", + "integrity": "sha512-FA3pe0WMMg0l3l3/pumjX6rhKJXikO6BBRb/3ZeprD0xql5CTx30D7cKl0neluNjeZ2bd+1C3KYSvL81i4Y77g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/jest-console": "^8.50.0", + "babel-jest": "^29.7.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@babel/core": ">=7", + "jest": ">=29" + } + }, + "node_modules/@wordpress/keycodes": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.50.0.tgz", + "integrity": "sha512-BtyCkA7pk5pMMx+dVjT10OuhQwr9ZjYY1LLcXJcYJjhhFsP16be2BiytrBnKDZj/479/icvEYwlooawZCPrxYQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/i18n": "^6.23.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18.3.27" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/npm-package-json-lint-config": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.50.0.tgz", + "integrity": "sha512-gI3vGO+R/kxUexTN2KfSy39ctg7QvOldZkEDZ90VVjK8Z6l46Uy5zvfMm94C6d5T7TgM9vcUkQghUd5ze0MXXA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "npm-package-json-lint": ">=6.0.0" + } + }, + "node_modules/@wordpress/postcss-plugins-preset": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.50.0.tgz", + "integrity": "sha512-uDpgDTGpHjjNHS+3E0hss0otRYeKVYulO2TA7jTpcXh9gikRCJYCQtFHh/eUCETrvSpzzfzIiKT7I048brWrcw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/base-styles": "^10.2.0", + "@wordpress/browserslist-config": "^6.50.0", + "autoprefixer": "^10.4.21", + "postcss-import": "^16.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/@wordpress/prettier-config": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.50.0.tgz", + "integrity": "sha512-Q48oFBORY0TiVWIebPOG0ynjUAq1Hi9gXKJ69xw2i/UwOwghLOXXFLdAQhS4bJXOw0cdV5xiBaXEQYU+82r5hA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "prettier": ">=3" + } + }, + "node_modules/@wordpress/primitives": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.50.0.tgz", + "integrity": "sha512-X/Vf1BUwPsxsyJq+z2UwjOHFoT4p1XI0VoxgN2JsbK0ZwbLPA9PQur1hGw9ujXqNZ4XG1U2rQ0aM+3yap7iC3g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/element": "^8.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18.3.27", + "react": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/primitives/node_modules/@wordpress/element": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.2.0.tgz", + "integrity": "sha512-cTnKRHUdZbYAd4RaJiUwhDKdQDDWY7VoGiwektCmhiBQF6WTFYlhsxl/dmHE0Sok3q7PJQwAg538XEb8WuTaew==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/deprecated": "^4.50.0", + "@wordpress/escape-html": "^3.50.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/priority-queue": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.50.0.tgz", + "integrity": "sha512-OyuAplepKQpNYr8z+nGGGNXWK2v2u9Bqnawkg7V5YkvOKYj/iBJNCiUxiWXOBMeLF1ze36RDR5LEcNDmXRev2Q==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "requestidlecallback": "^0.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/private-apis": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.50.0.tgz", + "integrity": "sha512-bCVLL9YuWfnPNNqlhl4eTKPipjWbJ8MzAlasJ/SR+h979MyeMFjNPapxKqsPqnJOhDEVSdCTkRN/Wa+Xk8qFdw==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/scripts": { + "version": "30.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.27.0.tgz", + "integrity": "sha512-gXGptazCxAaR7g8kcN5joj7B5fCm0VeBHOmnDBs2dbQ4W4F3tfzdg6CTEj8LonF9bWQXlSy3ku8EqWCdkSG9Xw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/core": "7.25.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@svgr/webpack": "^8.0.1", + "@wordpress/babel-preset-default": "^8.34.0", + "@wordpress/browserslist-config": "^6.34.0", + "@wordpress/dependency-extraction-webpack-plugin": "^6.34.0", + "@wordpress/e2e-test-utils-playwright": "^1.34.0", + "@wordpress/eslint-plugin": "^22.20.0", + "@wordpress/jest-preset-default": "^12.34.0", + "@wordpress/npm-package-json-lint-config": "^5.34.0", + "@wordpress/postcss-plugins-preset": "^5.34.0", + "@wordpress/prettier-config": "^4.34.0", + "@wordpress/stylelint-config": "^23.26.0", + "adm-zip": "^0.5.9", + "babel-jest": "29.7.0", + "babel-loader": "9.2.1", + "browserslist": "^4.21.10", + "chalk": "^4.0.0", + "check-node-version": "^4.1.0", + "copy-webpack-plugin": "^10.2.0", + "cross-spawn": "^7.0.6", + "css-loader": "^6.2.0", + "cssnano": "^6.0.1", + "cwd": "^0.10.0", + "dir-glob": "^3.0.1", + "eslint": "^8.3.0", + "expect-puppeteer": "^4.4.0", + "fast-glob": "^3.2.7", + "filenamify": "^4.2.0", + "jest": "^29.6.2", + "jest-dev-server": "^10.1.4", + "jest-environment-jsdom": "^29.6.2", + "jest-environment-node": "^29.6.2", + "json2php": "^0.0.9", + "markdownlint-cli": "^0.31.1", + "merge-deep": "^3.0.3", + "mini-css-extract-plugin": "^2.9.2", + "minimist": "^1.2.0", + "npm-package-json-lint": "^6.4.0", + "npm-packlist": "^3.0.0", + "postcss": "^8.4.5", + "postcss-loader": "^6.2.1", + "prettier": "npm:wp-prettier@3.0.3", + "puppeteer-core": "^23.10.1", + "react-refresh": "^0.14.0", + "read-pkg-up": "^7.0.1", + "resolve-bin": "^0.4.0", + "rtlcss": "^4.3.0", + "sass": "^1.54.0", + "sass-loader": "^16.0.3", + "schema-utils": "^4.2.0", + "source-map-loader": "^3.0.0", + "stylelint": "^16.8.2", + "terser-webpack-plugin": "^5.3.10", + "url-loader": "^4.1.1", + "webpack": "^5.97.0", + "webpack-bundle-analyzer": "^4.9.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + }, + "bin": { + "wp-scripts": "bin/wp-scripts.js" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": "^1.56.1", + "@wordpress/env": "^10.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@wordpress/env": { + "optional": true + } + } + }, + "node_modules/@wordpress/style-runtime": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/style-runtime/-/style-runtime-0.5.0.tgz", + "integrity": "sha512-hrXvdDUJpOzT1KIomgtysysgbc5bkkwAuJyEUAXiOCgVBXBeMkZlgfN19W0PuNY51j53K7VQ42txfayxgVmfgA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wordpress/stylelint-config": { + "version": "23.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.42.0.tgz", + "integrity": "sha512-k9lNU4sK3fdipdhHI2d/Rzd/NnfznPmOZ60ok8O8QhfCMZw2Jd1M8ckYqaIrHTKU62ZrcsugdfJlHodsdrxEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stylistic/stylelint-plugin": "^3.0.1", + "@wordpress/theme": "^0.17.0", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-recommended-scss": "^14.1.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "stylelint": "^16.8.2", + "stylelint-scss": "^6.4.0" + } + }, + "node_modules/@wordpress/theme": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@wordpress/theme/-/theme-0.17.0.tgz", + "integrity": "sha512-BU+PLBLxMBM4WJYrd1QEwy+IA64vUP+8IEEB9lLx2NfB+3/45vvuvXx9o3qPKRDHvtMtEaRArld4Kn05UIW9IA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/compose": "^8.2.0", + "@wordpress/deprecated": "^4.49.0", + "@wordpress/element": "^8.1.0", + "@wordpress/private-apis": "^1.49.0", + "@wordpress/style-runtime": "^0.5.0", + "colorjs.io": "^0.6.0", + "memize": "^2.1.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18.3.27", + "esbuild": "^0.27.2", + "postcss": "^8.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "stylelint": "^16.8.2", + "vite": "^7.3.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "postcss": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@wordpress/theme/node_modules/@wordpress/element": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.2.0.tgz", + "integrity": "sha512-cTnKRHUdZbYAd4RaJiUwhDKdQDDWY7VoGiwektCmhiBQF6WTFYlhsxl/dmHE0Sok3q7PJQwAg538XEb8WuTaew==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/deprecated": "^4.50.0", + "@wordpress/escape-html": "^3.50.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/undo-manager": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.50.0.tgz", + "integrity": "sha512-599I3GPXSMpmAE2jqzHteqypI7rdt5HPDAw8GrHMkDWR1T9gK2L3gObv8I1Hs4eSDM88wgAB0Ifnqpy7zPn5rw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/is-shallow-equal": "^5.50.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/warning": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.50.0.tgz", + "integrity": "sha512-6X3ioKOGfmRuZFngmG2QZZW9CBVMTBr5FXqs6Q3li5ryhnAqn3u/ocqsx556YnB5dYdVxK14TiH9o7DDElt8gw==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wp-playground/blueprints": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@wp-playground/blueprints/-/blueprints-3.1.43.tgz", + "integrity": "sha512-lytjwOREJcQ2+SZo1I+94kQJ2YwdcOMDq/0h+Kx5Yimud+tRuasn51QJGzXyQ6G4fSS5/zN6/XJJlG9PqbcRdA==", + "dev": true, + "dependencies": { + "@php-wasm/logger": "3.1.43", + "@php-wasm/progress": "3.1.43", + "@php-wasm/stream-compression": "3.1.43", + "@php-wasm/universal": "3.1.43", + "@php-wasm/util": "3.1.43", + "@php-wasm/web-service-worker": "3.1.43", + "@wp-playground/common": "3.1.43", + "@wp-playground/storage": "3.1.43", + "@wp-playground/wordpress": "3.1.43", + "ajv": "8.18.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wp-playground/cli": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@wp-playground/cli/-/cli-3.1.43.tgz", + "integrity": "sha512-PloKrA2F98+czv7UUjl55fFiZbV6chYgicm/gwGDt6UWJG8Rs0cPGAHf8hkOwFlVO5DUVNk+OsR+kulEPFG0Uw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/cli-util": "3.1.43", + "@php-wasm/logger": "3.1.43", + "@php-wasm/node": "3.1.43", + "@php-wasm/progress": "3.1.43", + "@php-wasm/universal": "3.1.43", + "@php-wasm/util": "3.1.43", + "@php-wasm/xdebug-bridge": "3.1.43", + "@wp-playground/blueprints": "3.1.43", + "@wp-playground/common": "3.1.43", + "@wp-playground/storage": "3.1.43", + "@wp-playground/tools": "3.1.43", + "@wp-playground/wordpress": "3.1.43", + "express": "4.22.2", + "fs-extra": "11.1.1", + "tmp-promise": "3.0.3", + "wasm-feature-detect": "1.8.0", + "yargs": "17.7.2" + }, + "bin": { + "wp-playground-cli": "wp-playground.js" + } + }, + "node_modules/@wp-playground/common": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@wp-playground/common/-/common-3.1.43.tgz", + "integrity": "sha512-IP6ofq3tufd4MQIZdK21OX3dPl0DuDTTy+PBdWDRhubZiKZFhKbKSLE/e0poWsu4RhJGqk+CUT30vPKJcmhskA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/universal": "3.1.43", + "@php-wasm/util": "3.1.43" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wp-playground/storage": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@wp-playground/storage/-/storage-3.1.43.tgz", + "integrity": "sha512-4Z7qDpYMCMpXnz70ZqIxhkuxhJOZmdREsVYwz47uoYCWC+LRjZIltXBxoSl+xtsRlFkzZaEJZpR3oiGTL2NKEA==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/stream-compression": "3.1.43", + "@php-wasm/universal": "3.1.43", + "@php-wasm/util": "3.1.43", + "@zip.js/zip.js": "2.7.57", + "isomorphic-git": "1.37.6", + "octokit": "3.1.2", + "pako": "^1.0.10" + } + }, + "node_modules/@wp-playground/tools": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@wp-playground/tools/-/tools-3.1.43.tgz", + "integrity": "sha512-JvCeZr12CDGlQM3AQq+sZXvqKRpiiFdvcy6Euk/tIOd+d60EoX0mcXPlCDdVfSh4E9uyKKdUk75L6hlvS/4/FQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wp-playground/blueprints": "3.1.43" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@wp-playground/wordpress": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@wp-playground/wordpress/-/wordpress-3.1.43.tgz", + "integrity": "sha512-tax9U/avoLgxbtxIBin68ugr+tDHWQ7ArQRA5qpRYjHPgqs9P4fUbeY13SHynkrJb9Rf01zcPd8GYMfhwoDHQw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@php-wasm/logger": "3.1.43", + "@php-wasm/universal": "3.1.43", + "@php-wasm/util": "3.1.43", + "@wp-playground/common": "3.1.43" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.57", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz", + "integrity": "sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.3.tgz", + "integrity": "sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz", + "integrity": "sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-stream/node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.41", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz", + "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.2.tgz", + "integrity": "sha512-lMskhnsW70yWHr4PhPeh2rvaIkLSaDpp+nmtbXBZaNKTXwxL73QOkW6HhbzqTImXjevn9TreGT4GACGBCGP9nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/check-node-version": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-4.2.1.tgz", + "integrity": "sha512-YYmFYHV/X7kSJhuN/QYHUu998n/TRuDe8UenM3+m5NrkiH670lb9ILqHIvBencvJc4SDh+XcbXMR4b+TtubJiw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "chalk": "^3.0.0", + "map-values": "^1.0.1", + "minimist": "^1.2.0", + "object-filter": "^1.0.2", + "run-parallel": "^1.1.4", + "semver": "^6.3.0" + }, + "bin": { + "check-node-version": "bin.js" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/check-node-version/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-launcher": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz", + "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "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, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/configstore": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", + "integrity": "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.20.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/countup.js": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.10.1.tgz", + "integrity": "sha512-UHW/BsPDgVZfN919D4iu2HPv+jJxZUuaR3EhT0ScFZaD446GiQtkMTgOnOyJXdA2AewdlyUk9UvERlTHTbVpcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.5.tgz", + "integrity": "sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/css-declaration-sorter": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/devtools-protocol": { + "version": "0.0.1507524", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1507524.tgz", + "integrity": "sha512-OjaNE7qpk6GRTXtqQjAE5bGx6+c4F1zZH0YXtpZQLM92HNXx4zMAaqlKhP4T52DosG6hDW8gPMNhGOF8xbwk/w==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.385", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz", + "integrity": "sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/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, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz", + "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", + "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz", + "integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jest/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "46.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", + "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.41.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-playwright": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.15.3.tgz", + "integrity": "sha512-LQMW5y0DLK5Fnpya7JR1oAYL2/7Y9wDiYw6VZqlKqcRGSgjbVKNqxraphk7ra1U3Bb5EK444xMgUlQPbMg2M1g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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, + "license": "MIT" + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect-puppeteer": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz", + "integrity": "sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.1.tgz", + "integrity": "sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", + "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^1.0.1", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.4.1", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-file-up": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", + "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-parent-dir": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", + "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", + "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-process": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.11.tgz", + "integrity": "sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "MIT", + "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/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-ext-extra-prebuilt": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fs-ext-extra-prebuilt/-/fs-ext-extra-prebuilt-2.2.7.tgz", + "integrity": "sha512-Q7rayYRBDIvDF01HWOwSSjoaP+05N1g+o3BXL1Zf8Frw2JkjSmi4EtvCBITuW30l6hB2m2TW1pehdh8wyU/+gw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.24.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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, + "license": "MIT", + "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-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gettext-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", + "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encoding": "^0.1.12", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "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, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", + "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unsafe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", + "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-git": { + "version": "1.37.6", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.37.6.tgz", + "integrity": "sha512-qr1NFCPsVTZ6YGqTXw0CzamnsHyH9QQ1OTEfeXIweSljRUMzuHFCJdUn0wc6OcjtTDns6knxjPb7N6LmJeftOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^4.0.0", + "sha.js": "^2.4.12", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=14.17" + } + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-dev-server": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-10.1.4.tgz", + "integrity": "sha512-bGQ6sedNGtT6AFHhCVqGTXMPz7UyJi/ZrhNBgyqsP0XU9N8acCEIfqZEA22rOaZ+NdEVsaltk6tL7UT6aXfI7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.7", + "prompts": "^2.4.2", + "spawnd": "^10.1.4", + "tree-kill": "^1.2.2", + "wait-on": "^8.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz", + "integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json2php": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.9.tgz", + "integrity": "sha512-fQMYwvPsQt8hxRnCGyg1r2JVi6yL8Um0DIIawiKiMK9yhAAkcRNj5UsBWoaFvFzPpcWbgw9L6wzj+UMYA702Mw==", + "dev": true, + "license": "BSD" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/legacy-javascript": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz", + "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lighthouse": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.8.2.tgz", + "integrity": "sha512-+5SKYzVaTFj22MgoYDPNrP9tlD2/Ay7j3SxPSFD9FpPyVxGr4UtOQGKyrdZ7wCmcnBaFk0mCkPfARU3CsE0nvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.59", + "@sentry/node": "^9.28.1", + "axe-core": "^4.10.3", + "chrome-launcher": "^1.2.0", + "configstore": "^7.0.0", + "csp_evaluator": "1.1.5", + "devtools-protocol": "0.0.1507524", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.2", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^24.17.1", + "robots-parser": "^3.0.1", + "speedline-core": "^1.4.3", + "third-party-web": "^0.27.0", + "tldts-icann": "^7.0.12", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.16" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", + "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lighthouse/node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/lighthouse/node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "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/lighthouse/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lighthouse/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/lighthouse/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/map-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", + "integrity": "sha512-BbShUnr5OartXJe1GeccAWtfro11hhgNJg6G9/UtWKjVGvV5U4C09cg5nk8JUevhXODaXY+hQ3xxMUKSs62ONQ==", + "dev": true, + "license": "Public Domain" + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdownlint": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", + "integrity": "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==", + "dev": true, + "license": "MIT", + "dependencies": { + "markdown-it": "12.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.31.1.tgz", + "integrity": "sha512-keIOMwQn+Ch7MoBwA+TdkyVMuxAeZFEGmIIlvwgV0Z1TGS5MxPnRr29XCLhkNzCHU+uNKGjU+VEjLX+Z9kli6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "~9.0.0", + "get-stdin": "~9.0.0", + "glob": "~7.2.0", + "ignore": "~5.2.0", + "js-yaml": "^4.1.0", + "jsonc-parser": "~3.0.0", + "markdownlint": "~0.25.1", + "markdownlint-rule-helpers": "~0.16.0", + "minimatch": "~3.0.5", + "run-con": "~1.2.10" + }, + "bin": { + "markdownlint": "markdownlint.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/markdownlint-cli/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/markdownlint-cli/node_modules/commander": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz", + "integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/markdownlint-cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/markdownlint-cli/node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/markdownlint-cli/node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/markdownlint-cli/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/markdownlint-cli/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdownlint-cli/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/markdownlint-rule-helpers": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz", + "integrity": "sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memize": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.1.tgz", + "integrity": "sha512-8Nl+i9S5D6KXnruM03Jgjb+LwSupvR13WBr4hJegaaEyobvowCVupi79y2WSiWvO1mzBWxPwEYE5feCe8vyA5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minimist-options/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mousetrap": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", + "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==", + "dev": true, + "license": "Apache-2.0 WITH LLVM-exception" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-package-json-lint": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-6.4.0.tgz", + "integrity": "sha512-cuXAJJB1Rdqz0UO6w524matlBqDBjcNt7Ru+RDIu4y6RI1gVqiWBnylrK8sPRk81gGBA0X8hJbDXolVOoTc+sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "chalk": "^4.1.2", + "cosmiconfig": "^8.0.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "ignore": "^5.2.0", + "is-plain-obj": "^3.0.0", + "jsonc-parser": "^3.2.0", + "log-symbols": "^4.1.0", + "meow": "^9.0.0", + "plur": "^4.0.0", + "semver": "^7.3.8", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1", + "type-fest": "^3.2.0", + "validate-npm-package-name": "^5.0.0" + }, + "bin": { + "npmPkgJsonLint": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/npm-package-json-lint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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/npm-package-json-lint/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, + "license": "MIT" + }, + "node_modules/npm-package-json-lint/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-package-json-lint/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-packlist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz", + "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.6", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-packlist/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-packlist/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-filter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", + "integrity": "sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/octokit": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz", + "integrity": "sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/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, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/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, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/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, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", + "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-import": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz", + "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-loader/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "name": "wp-prettier", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", + "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/requestidlecallback": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/requestidlecallback/-/requestidlecallback-0.3.0.tgz", + "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-bin": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/resolve-bin/-/resolve-bin-0.4.3.tgz", + "integrity": "sha512-9u8TMpc+SEHXxQXblXHz5yRvRZERkCZimFN9oz85QI3uhkh7nqfjm6OGTLg+8vucpXGcY4jLK6WkylPmt7GSvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-parent-dir": "~0.3.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-con": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.12.tgz", + "integrity": "sha512-5257ILMYIF4RztL9uoZ7V9Q97zHtNHn5bN3NobeAnzB1P3ASLgg8qocM2u+R18ttp+VEM78N2LK8XcNVtnSRrg==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~3.0.0", + "minimist": "^1.2.8", + "strip-json-comments": "~3.1.1" + }, + "bin": { + "run-con": "cli.js" + } + }, + "node_modules/run-con/node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.101.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", + "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.8.tgz", + "integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/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, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/source-map-loader/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, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawnd": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-10.1.4.tgz", + "integrity": "sha512-drqHc0mKJmtMsiGMOCwzlc5eZ0RPtRvT7tQAluW2A0qUc0G7TQ8KLcn3E6K5qzkLkH2UkS3nYQiVGULvvsD9dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "signal-exit": "^4.1.0", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/stylelint": { + "version": "16.26.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz", + "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-syntax-patches-for-csstree": "^1.0.19", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.1", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^7.0.5", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.2.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz", + "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.1.0.tgz", + "integrity": "sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^14.0.1", + "stylelint-scss": "^6.4.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.6.1" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-scss": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.14.0.tgz", + "integrity": "sha512-ZKmHMZolxeuYsnB+PCYrTpFce0/QWX9i9gh0hPXzp73WjuIMqUpzdQaBCrKoLWh6XtCFSaNDErkMPqdjy1/8aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.1", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mdn-data": "^2.25.0", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-selector-parser": "^7.1.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.8.2" + } + }, + "node_modules/stylelint-scss/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/stylelint/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/stylelint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/stylelint/node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stylelint/node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/stylelint/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/stylelint/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylelint/node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgo/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/svgo/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, + "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, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tannin": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", + "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tannin/plural-forms": "^1.1.0" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream/node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/third-party-web": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.27.0.tgz", + "integrity": "sha512-h0JYX+dO2Zr3abCQpS6/uFjujaOjA1DyDzGQ41+oFn9VW/ARiq9g5ln7qEP9+BTzDpOMyIfsfj4OvfgXAsMUSA==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tldts-core": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.6.tgz", + "integrity": "sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.4.6.tgz", + "integrity": "sha512-T7sT7XcBzPJY5OJo7DIFiH9Wbh7iO1rBXHq681Caghnke9EjXTNXsuJ/re03TzGBB7DWWzQ6sDcYtM1rfRMHVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.6" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/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, + "license": "MIT" + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/wait-on": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", + "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.12.1", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.108.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.3.tgz", + "integrity": "sha512-hOpaCHmQVVY66IVTjofnH14IgSdmod2aquSGHGuYig/OIdWge01Hk2Wt988DZcwXumFUT4+FvJY5N+ikl8o/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-merge/node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-merge/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-merge/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-merge/node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/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, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "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/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..95b21d5 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "airo-wp", + "version": "0.2.4", + "private": true, + "files": [ + "data/", + "dependencies/", + "dist/", + "functions/", + "includes/", + "patterns/", + "src/", + "vendor/", + "airo-wp.php", + "composer.json", + "readme.txt" + ], + "scripts": { + "build": "wp-scripts build", + "start": "wp-scripts start", + "test": "playwright test", + "test:debug": "playwright test --debug" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@wordpress/e2e-test-utils-playwright": "^1.50.0", + "@wordpress/icons": "^10.0.0", + "@wordpress/scripts": "^30.0.0", + "@wp-playground/cli": "3.1.43", + "classnames": "^2.3.2", + "countup.js": "^2.9.0", + "glob": "^11.0.0", + "leaflet": "^1.9.4", + "typescript": "^5.4.0" + } +} diff --git a/patterns/contact/contact-consultation-form.php b/patterns/contact/contact-consultation-form.php new file mode 100644 index 0000000..4b50d77 --- /dev/null +++ b/patterns/contact/contact-consultation-form.php @@ -0,0 +1,83 @@ + __( 'Consultation Contact Form', 'airo-wp' ), + 'categories' => array( 'airo-wp-contact' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Contact Us

+ + + +

Begin Your Journey to Extraordinary Living

+ + + +

Whether you are searching for your dream home or considering selling your property, our team is ready to provide the exceptional service you deserve.

+ + + +
+
+ + +

+1 (310) 555-0100

+
+ + + +
+ + +

inquiries@luxuryestates.com

+
+ + + +
+ + +

9876 Wilshire Blvd, Beverly Hills, CA 90210

+
+
+
+ + + +
+

Schedule a Consultation

+ + + +
+
+ + + + + + + +
+ + + +
+
+
+
+
+', +); diff --git a/patterns/contact/contact-consultation.php b/patterns/contact/contact-consultation.php new file mode 100644 index 0000000..c312ba9 --- /dev/null +++ b/patterns/contact/contact-consultation.php @@ -0,0 +1,93 @@ + __( 'Consultation Contact Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-contact' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Schedule Your Free Consultation

+ + + +

Take the first step toward resolving your legal matter. Contact us today to schedule a confidential consultation with one of our experienced attorneys.

+ + + +
+
+ + +

123 Legal Plaza, Suite 500, Business City, ST 12345

+
+ + + +
+ + +

(555) 123-4567

+
+ + + +
+ + +

contact@professionalfirm.com

+
+ + + +
+ + +

Monday - Friday: 8:30 AM - 6:00 PM

+
+
+
+ + + +
+
+

Request a Consultation

+ + + +
+
+ + + + + + + +
+ + + +
+ + + +
+
+
+
+
+
+', +); diff --git a/patterns/contact/contact-form.php b/patterns/contact/contact-form.php new file mode 100644 index 0000000..389a8d3 --- /dev/null +++ b/patterns/contact/contact-form.php @@ -0,0 +1,69 @@ + __( 'Contact Form with Map', 'airo-wp' ), + 'categories' => array( 'airo-wp-contact' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Get In Touch

+ + + +

We would love to hear from you. Send us a message and we will respond as soon as possible.

+
+ + + +
+
+
+
+ + + + + + + +
+ + + +
+
+
+ + + +
+
+ + + +
+

Our Office

+ + + +

123 Business Street
New York, NY 10001
United States

+ + + +

Phone: +1 (555) 123-4567
Email: hello@example.com

+
+
+
+
+', +); diff --git a/patterns/contact/contact-map-split.php b/patterns/contact/contact-map-split.php new file mode 100644 index 0000000..f0632cc --- /dev/null +++ b/patterns/contact/contact-map-split.php @@ -0,0 +1,105 @@ + __( 'Contact Map Split', 'airo-wp' ), + 'categories' => array( 'airo-wp-contact' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Get In Touch

+ + + +

Contact Us

+ + + +

Have a question or want to work together? We would love to hear from you.

+
+ + + +
+
+

Send Us a Message

+ + + +
+
+ + + + + + + +
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+ + +

123 Business Avenue, New York, NY 10001

+
+
+ + + +
+
+ + +

+1 (555) 123-4567

+
+
+ + + +
+
+ + +

hello@company.com

+
+
+ + + +
+
+ + +

Mon - Fri: 9:00 AM - 6:00 PM

+
+
+
+
+
+
+', +); diff --git a/patterns/contact/contact-split.php b/patterns/contact/contact-split.php new file mode 100644 index 0000000..2b6585b --- /dev/null +++ b/patterns/contact/contact-split.php @@ -0,0 +1,89 @@ + __( 'Contact Split', 'airo-wp' ), + 'categories' => array( 'airo-wp-contact' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Let us Talk

+ + + +

Have a question or want to work together? We are here to help you bring your ideas to life.

+ + + +
+
+

Email us: hello@example.com

+
+ + + +
+

Call Us: 123-456-7890

+
+ + + +
+

Visit Us: Address Here

+
+ + + +
+

Hours: Mon-Fri: 9AM - 6PM EST

+
+
+ + + + +
+ + + +
+

Send a Message

+ + + +
+
+ + + +
+ + + + + + + +
+
+
+
+
+', +); diff --git a/patterns/content/content-about-blobs.php b/patterns/content/content-about-blobs.php new file mode 100644 index 0000000..ccb68f7 --- /dev/null +++ b/patterns/content/content-about-blobs.php @@ -0,0 +1,103 @@ + __( 'About Section with Blobs', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+
+ + + + +

+
+
+ + + +
+
+ + +

Building the Future of Digital Experiences

+
+ + + +

We are a team of passionate designers and developers dedicated to creating exceptional digital products. Our mission is to help businesses transform their ideas into reality.

+ + + + + + + +
+
+ + +

Our Mission

+ + + +

Empowering businesses with innovative solutions.

+
+ + + +
+ + +

Our Vision

+ + + +

Leading the digital transformation revolution.

+
+ + + +
+ + +

Our Values

+ + + +

Integrity, innovation, and client success.

+
+ + + +
+ + +

Our Team

+ + + +

50+ experts across design and development.

+
+
+ + + +
+
+
+
+', +); diff --git a/patterns/content/content-about-stats.php b/patterns/content/content-about-stats.php new file mode 100644 index 0000000..4a88838 --- /dev/null +++ b/patterns/content/content-about-stats.php @@ -0,0 +1,61 @@ + __( 'About Section with Stats', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

About Me

+ + + +

Designing with Purpose, Creating with Passion

+ + + +

With over 10 years of experience in product design, I have helped companies ranging from early-stage startups to Fortune 500 companies create meaningful digital experiences.

+ + + +

I believe great design is invisible - it should feel intuitive and delightful without getting in the way. My approach combines user research, strategic thinking, and pixel-perfect execution to deliver results that matter.

+ + + +

When not designing, you will find me mentoring aspiring designers, speaking at conferences, or exploring hiking trails with my camera.

+
+ + + +
+
+
0
Years Experience
+ + + +
0
Projects Delivered
+ + + +
0
Happy Clients
+ + + +
0
Design Awards
+
+
+
+
+', +); diff --git a/patterns/content/content-alternating-rows.php b/patterns/content/content-alternating-rows.php new file mode 100644 index 0000000..d4384fd --- /dev/null +++ b/patterns/content/content-alternating-rows.php @@ -0,0 +1,159 @@ + __( 'Alternating Content Rows', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

How We Help You Succeed

+ + + +

A comprehensive approach to achieving your goals

+
+ + + +
+
+
Strategy planning
+
+ + + +
+
+

STEP 01

+ + + +

Strategic Planning

+ + + +

We start by understanding your business goals and creating a comprehensive roadmap for success. Our team analyzes market trends and competitive landscapes.

+ + + +
+
+

Market research & analysis

+
+ + + +
+

Goal setting & KPIs

+
+ + + +
+

Resource allocation

+
+
+
+
+
+ + + +
+
+
+

STEP 02

+ + + +

Design & Development

+ + + +

Our expert team brings your vision to life with cutting-edge design and robust development practices that ensure scalability and performance.

+ + + +
+
+

User-centered design

+
+ + + +
+

Agile development

+
+ + + +
+

Quality assurance

+
+
+
+
+ + + +
+
Design process
+
+
+ + + +
+
+
Launch and growth
+
+ + + +
+
+

STEP 03

+ + + +

Launch & Growth

+ + + +

We ensure a smooth launch and provide ongoing support to help you grow. Our data-driven approach helps optimize performance continuously.

+ + + +
+
+

Seamless deployment

+
+ + + +
+

Performance monitoring

+
+ + + +
+

Continuous optimization

+
+
+
+
+
+
+', +); diff --git a/patterns/content/content-brand-trust-bar.php b/patterns/content/content-brand-trust-bar.php new file mode 100644 index 0000000..f057bff --- /dev/null +++ b/patterns/content/content-brand-trust-bar.php @@ -0,0 +1,39 @@ + __( 'Brand Trust Bar', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+

Trusted by Leading Brands

+ + + +
+

VOGUE

+ + + +

NIKE

+ + + +

SPOTIFY

+ + + +

AIRBNB

+
+
+', +); diff --git a/patterns/content/content-campaign-progress.php b/patterns/content/content-campaign-progress.php new file mode 100644 index 0000000..3f9af75 --- /dev/null +++ b/patterns/content/content-campaign-progress.php @@ -0,0 +1,55 @@ + __( 'Campaign Progress Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Current Campaign

+ + + +

Help Us Reach Our Goal

+ + + +

We are building 10 new schools in rural communities. Every donation brings us closer to making education accessible to 5,000 more children.

+
+ + + +
+
75%
+ + + + +
+
+', +); diff --git a/patterns/content/content-client-logos.php b/patterns/content/content-client-logos.php new file mode 100644 index 0000000..2f9b65f --- /dev/null +++ b/patterns/content/content-client-logos.php @@ -0,0 +1,43 @@ + __( 'Client Logos Row', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+

Previously worked with

+ + + +
+

Google

+ + + +

Spotify

+ + + +

Airbnb

+ + + +

Stripe

+ + + +

Notion

+
+
+', +); diff --git a/patterns/content/content-coaching-split.php b/patterns/content/content-coaching-split.php new file mode 100644 index 0000000..5cea079 --- /dev/null +++ b/patterns/content/content-coaching-split.php @@ -0,0 +1,77 @@ + __( 'Coaching Split Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+
Personal training session
+
+ + + +
+

Personal Training

+ + + +

One-on-One Coaching

+ + + +

Get personalized attention and a customized workout plan designed specifically for your goals, fitness level, and schedule.

+ + + +
+
+ + +

Custom workout plans

+
+ + + +
+ + +

Nutrition guidance

+
+ + + +
+ + +

Progress tracking

+
+ + + +
+ + +

Flexible scheduling

+
+
+ + + + +
+
+
+', +); diff --git a/patterns/content/content-company-logos.php b/patterns/content/content-company-logos.php new file mode 100644 index 0000000..70f1acb --- /dev/null +++ b/patterns/content/content-company-logos.php @@ -0,0 +1,43 @@ + __( 'Company Trust Logos', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+

TRUSTED BY INNOVATIVE TEAMS WORLDWIDE

+ + + +
+

STRIPE

+ + + +

NOTION

+ + + +

FIGMA

+ + + +

VERCEL

+ + + +

LINEAR

+
+
+', +); diff --git a/patterns/content/content-course-cards.php b/patterns/content/content-course-cards.php new file mode 100644 index 0000000..3e7658c --- /dev/null +++ b/patterns/content/content-course-cards.php @@ -0,0 +1,139 @@ + __( 'Course Cards with Ratings', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+
+

Featured Courses

+ + + +

Top Rated This Month

+
+ + + + +
+
+ + + +
+
+
Web development course
+ + + +
+
+ + +

★★★★★ 4.9

+
+ + + +

Complete Web Development Bootcamp

+ + + +

Dr. Angela Yu • 65 hours

+ + + +
+

$89.99

+ + + +

$199.99

+
+
+
+ + + +
+
Data science course
+ + + +
+
+ + +

★★★★★ 4.8

+
+ + + +

Python for Data Science & Machine Learning

+ + + +

Jose Portilla • 45 hours

+ + + +
+

$94.99

+ + + +

$179.99

+
+
+
+ + + +
+
UI/UX design course
+ + + +
+
+ + +

★★★★★ 4.9

+
+ + + +

UI/UX Design Masterclass

+ + + +

Sarah Chen • 38 hours

+ + + +
+

$79.99

+ + + +

$149.99

+
+
+
+
+
+', +); diff --git a/patterns/content/content-education-stats.php b/patterns/content/content-education-stats.php new file mode 100644 index 0000000..c8eb943 --- /dev/null +++ b/patterns/content/content-education-stats.php @@ -0,0 +1,35 @@ + __( 'Education Stats Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
0
Students Enrolled
+ + + +
0
Expert Courses
+ + + +
0
Certified Instructors
+ + + +
0
Satisfaction Rate
+
+
+', +); diff --git a/patterns/content/content-event-schedule.php b/patterns/content/content-event-schedule.php new file mode 100644 index 0000000..ad66486 --- /dev/null +++ b/patterns/content/content-event-schedule.php @@ -0,0 +1,185 @@ + __( 'Event Schedule Timeline', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Event Schedule

+ + + +

Three Days of Innovation

+
+ + + +
+ + + + + + + + + +
+
+', +); diff --git a/patterns/content/content-event-stats.php b/patterns/content/content-event-stats.php new file mode 100644 index 0000000..09f4ff7 --- /dev/null +++ b/patterns/content/content-event-stats.php @@ -0,0 +1,35 @@ + __( 'Event Stats Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
0
World-Class Speakers
+ + + +
0
Days of Innovation
+ + + +
0
Sessions & Workshops
+ + + +
0
Expected Attendees
+
+
+', +); diff --git a/patterns/content/content-fitness-stats.php b/patterns/content/content-fitness-stats.php new file mode 100644 index 0000000..f4a9c12 --- /dev/null +++ b/patterns/content/content-fitness-stats.php @@ -0,0 +1,35 @@ + __( 'Fitness Stats Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
0
Active Members
+ + + +
0
Expert Trainers
+ + + +
0
Weekly Classes
+ + + +
0
Years Experience
+
+
+', +); diff --git a/patterns/content/content-growth-stats.php b/patterns/content/content-growth-stats.php new file mode 100644 index 0000000..d23a551 --- /dev/null +++ b/patterns/content/content-growth-stats.php @@ -0,0 +1,45 @@ + __( 'Growth Stats Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Trusted by Growing Companies

+ + + +

See the impact we have made together

+
+ + + +
+
0
Active Users
+ + + +
0
Uptime SLA
+ + + +
0
Integrations
+ + + +
0
Support
+
+
+', +); diff --git a/patterns/content/content-image-overlay.php b/patterns/content/content-image-overlay.php new file mode 100644 index 0000000..16e4d78 --- /dev/null +++ b/patterns/content/content-image-overlay.php @@ -0,0 +1,43 @@ + __( 'Image with Text Overlay', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+
+ + +

Transform Your Business with Modern Solutions

+
+ + + +

Discover how our cutting-edge technology can help you achieve your goals and stay ahead of the competition.

+ + + +
+
+ + + +
+
+
+
+
+', +); diff --git a/patterns/content/content-impact-stats.php b/patterns/content/content-impact-stats.php new file mode 100644 index 0000000..543466e --- /dev/null +++ b/patterns/content/content-impact-stats.php @@ -0,0 +1,35 @@ + __( 'Impact Stats Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
0
Lives Changed
+ + + +
0
Countries Reached
+ + + +
0
Volunteers
+ + + +
0
Raised
+
+
+', +); diff --git a/patterns/content/content-impact-stories.php b/patterns/content/content-impact-stories.php new file mode 100644 index 0000000..291002c --- /dev/null +++ b/patterns/content/content-impact-stories.php @@ -0,0 +1,89 @@ + __( 'Impact Stories Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Success Stories

+ + + +

Real Impact, Real Stories

+
+ + + +
+
+
+
Maria with her family
+ + + +

Maria\\\'s Journey to Self-Sufficiency

+ + + +

"Thanks to the microloan program, I was able to start my own tailoring business. Now I can provide for my children and send them to school. This organization gave me hope when I had none."

+ + + +

— Maria, Guatemala

+
+
+ + + +
+
+
Village with clean water
+ + + +

Clean Water for Kibera Village

+ + + +

"Before the well was built, we walked 3 miles every day for water. Now our children are healthier and can focus on their education instead of carrying water."

+ + + +

— Village Elder, Kenya

+
+
+ + + +
+
+
Students in classroom
+ + + +

First in Her Family to Graduate

+ + + +

"The scholarship changed everything. I became the first person in my family to finish high school. Now I am studying to become a doctor so I can help my community."

+ + + +

— Amara, Nigeria

+
+
+
+
+', +); diff --git a/patterns/content/content-legacy-stats.php b/patterns/content/content-legacy-stats.php new file mode 100644 index 0000000..07a1130 --- /dev/null +++ b/patterns/content/content-legacy-stats.php @@ -0,0 +1,53 @@ + __( 'Legacy Stats with Description', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

About Us

+ + + +

A Legacy of Excellence in Luxury Real Estate

+ + + +

For over two decades, we have been the trusted advisors for clients seeking extraordinary properties. Our deep expertise in luxury markets, combined with unparalleled discretion and personalized service, sets us apart.

+ + + +

We understand that finding the perfect home is about more than square footage and amenities. It is about discovering a residence that reflects your lifestyle, aspirations, and vision for the future.

+ + + +
+
0
In Sales Volume
+ + + +
0
Properties Sold
+
+
+ + + +
+
Luxury home interior
+
+
+
+', +); diff --git a/patterns/content/content-location-hours.php b/patterns/content/content-location-hours.php new file mode 100644 index 0000000..441dd5d --- /dev/null +++ b/patterns/content/content-location-hours.php @@ -0,0 +1,83 @@ + __( 'Location and Hours', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Visit Us

+ + + +

Location & Hours

+ + + +
+
+ + +
+

Address

+ + + +

456 Culinary Lane
Downtown District, City 12345

+
+
+ + + +
+ + +
+

Hours

+ + + +

Tue - Thu: 5:00 PM - 10:00 PM
Fri - Sat: 5:00 PM - 11:00 PM
Sun: 4:00 PM - 9:00 PM
Monday: Closed

+
+
+ + + +
+ + +
+

Reservations

+ + + +

(555) 987-6543

+
+
+
+ + + + +
+ + + +
+
+
+
+
+', +); diff --git a/patterns/content/content-logo-cloud.php b/patterns/content/content-logo-cloud.php new file mode 100644 index 0000000..c5484e5 --- /dev/null +++ b/patterns/content/content-logo-cloud.php @@ -0,0 +1,43 @@ + __( 'Logo Cloud Partners', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+

Trusted by Industry Leaders

+ + + +
+
Google
+ + + +
Amazon
+ + + +
Microsoft
+ + + +
Netflix
+ + + +
Apple
+
+
+', +); diff --git a/patterns/content/content-menu-dishes.php b/patterns/content/content-menu-dishes.php new file mode 100644 index 0000000..91e0e93 --- /dev/null +++ b/patterns/content/content-menu-dishes.php @@ -0,0 +1,99 @@ + __( 'Menu Signature Dishes', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Chef Favorites

+ + + +

Signature Dishes

+ + + +

Our most beloved creations, made with the freshest seasonal ingredients.

+
+ + + +
+
+
Grilled Salmon
+ + + +
+

Herb-Crusted Salmon

+ + + +

$34

+
+ + + +

Wild-caught salmon with lemon herb crust, served with seasonal vegetables and saffron risotto.

+
+ + + +
+
Beef Tenderloin
+ + + +
+

Prime Beef Tenderloin

+ + + +

$48

+
+ + + +

Aged tenderloin with red wine reduction, truffle mashed potatoes, and grilled asparagus.

+
+ + + +
+
Lobster Risotto
+ + + +
+

Lobster Risotto

+ + + +

$42

+
+ + + +

Maine lobster tail atop creamy Arborio rice with saffron, white wine, and fresh herbs.

+
+
+ + + + +
+', +); diff --git a/patterns/content/content-mission-statement.php b/patterns/content/content-mission-statement.php new file mode 100644 index 0000000..4b68600 --- /dev/null +++ b/patterns/content/content-mission-statement.php @@ -0,0 +1,47 @@ + __( 'Mission Statement Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Our Mission

+ + + +

Empowering Communities, One Life at a Time

+ + + +

We believe everyone deserves access to clean water, education, healthcare, and economic opportunity. Through sustainable programs and community partnerships, we work to break the cycle of poverty.

+ + + +

Our approach focuses on long-term solutions that empower communities to thrive independently. We invest in local leaders, provide training and resources, and measure our impact to ensure every dollar makes a difference.

+ + + + +
+ + + +
+
Children receiving education
+
+
+
+', +); diff --git a/patterns/content/content-portfolio-cards.php b/patterns/content/content-portfolio-cards.php new file mode 100644 index 0000000..1d0cf94 --- /dev/null +++ b/patterns/content/content-portfolio-cards.php @@ -0,0 +1,47 @@ + __( 'Portfolio Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Our Work

+ + + +

Selected projects we are proud of

+
+ + + +
+
+ +
+ + + +
+ +
+ + + +
+ +
+
+
+', +); diff --git a/patterns/content/content-process-steps.php b/patterns/content/content-process-steps.php new file mode 100644 index 0000000..dba88d0 --- /dev/null +++ b/patterns/content/content-process-steps.php @@ -0,0 +1,93 @@ + __( 'Process Steps', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

How It Works

+ + + +

Get started in just four simple steps

+
+ + + +
+
+
+

01

+
+ + + +

Create Account

+ + + +

Sign up for free in under a minute. No credit card required to get started.

+
+ + + +
+
+

02

+
+ + + +

Configure Settings

+ + + +

Customize your workspace with our intuitive setup wizard and preferences.

+
+ + + +
+
+

03

+
+ + + +

Import Data

+ + + +

Easily import your existing data from spreadsheets or other platforms.

+
+ + + +
+
+

04

+
+ + + +

Launch & Grow

+ + + +

You are all set! Start using the platform and watch your business grow.

+
+
+
+', +); diff --git a/patterns/content/content-product-cards.php b/patterns/content/content-product-cards.php new file mode 100644 index 0000000..e61114d --- /dev/null +++ b/patterns/content/content-product-cards.php @@ -0,0 +1,123 @@ + __( 'Product Cards Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+
+

Featured Products

+ + + +

Best Sellers

+
+ + + + +
+
+ + + +
+
+
Premium Sneakers
+ + + +
+

Footwear

+ + + +

Premium Sneakers

+ + + +
+

$149

+ + + +

$99

+
+
+
+ + + +
+
Classic Watch
+ + + +
+

Accessories

+ + + +

Classic Watch

+ + + +

$249

+
+
+ + + +
+
Leather Backpack
+ + + +
+

Bags

+ + + +

Leather Backpack

+ + + +

$189

+
+
+ + + +
+
Designer Sunglasses
+ + + +
+

Eyewear

+ + + +

Designer Sunglasses

+ + + +

$129

+
+
+
+
+', +); diff --git a/patterns/content/content-product-demo.php b/patterns/content/content-product-demo.php new file mode 100644 index 0000000..dea69e9 --- /dev/null +++ b/patterns/content/content-product-demo.php @@ -0,0 +1,69 @@ + __( 'Product Demo Showcase', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Product Showcase

+ + + +

See It in Action

+
+ + + +
+
+

Dashboard

+ + + +

Powerful analytics at your fingertips

+
+ + + +
+

Reports

+ + + +

Generate insights in seconds

+
+ + + +
+

Team Hub

+ + + +

Collaborate in real-time

+
+ + + +
+

Integrations

+ + + +

Connect with 100+ tools

+
+
+
+', +); diff --git a/patterns/content/content-professional-stats.php b/patterns/content/content-professional-stats.php new file mode 100644 index 0000000..ad7fa11 --- /dev/null +++ b/patterns/content/content-professional-stats.php @@ -0,0 +1,35 @@ + __( 'Professional Stats Bar', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+ +
+', +); diff --git a/patterns/content/content-progress-skills.php b/patterns/content/content-progress-skills.php new file mode 100644 index 0000000..cfd0437 --- /dev/null +++ b/patterns/content/content-progress-skills.php @@ -0,0 +1,55 @@ + __( 'Skills Progress Bars', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Our Expertise

+ + + +

We have honed our skills over years of experience to deliver exceptional results in every project we undertake.

+ + + + +
+ + + +
+
WordPress Development - 95%
+ + + +
React & JavaScript - 90%
+ + + +
UI/UX Design - 85%
+ + + +
Performance Optimization - 80%
+ + + +
SEO & Accessibility - 88%
+
+
+
+', +); diff --git a/patterns/content/content-quote-highlight.php b/patterns/content/content-quote-highlight.php new file mode 100644 index 0000000..b50013c --- /dev/null +++ b/patterns/content/content-quote-highlight.php @@ -0,0 +1,45 @@ + __( 'Quote Highlight', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+ + +

The best way to predict the future is to create it. Innovation distinguishes between a leader and a follower.

+ + + +
+ + + +
+
Steve Jobs
+ + + +
+

Steve Jobs

+ + + +

Co-founder, Apple Inc.

+
+
+
+
+', +); diff --git a/patterns/content/content-slider-cards.php b/patterns/content/content-slider-cards.php new file mode 100644 index 0000000..e2c3d08 --- /dev/null +++ b/patterns/content/content-slider-cards.php @@ -0,0 +1,119 @@ + __( 'Content Cards Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Explore Our Features

+ + + +

Drag to explore the cards or use navigation arrows

+
+ + + +
+
+
+ + +

Lightning Fast

+ + + +

Our optimized infrastructure ensures sub-second load times for all your pages and assets.

+ + + +
+
+
+ + + +
+
+ + +

Enterprise Security

+ + + +

Bank-level encryption and security protocols protect your data around the clock.

+ + + +
+
+
+ + + +
+
+ + +

Team Collaboration

+ + + +

Work together seamlessly with real-time editing and commenting features.

+ + + +
+
+
+ + + +
+
+ + +

Analytics Dashboard

+ + + +

Track performance metrics and gain actionable insights with our powerful dashboard.

+ + + +
+
+
+ + + +
+
+ + +

Global CDN

+ + + +

Content delivered from edge locations worldwide for minimal latency everywhere.

+ + + +
+
+
+
+
+', +); diff --git a/patterns/content/content-split-image.php b/patterns/content/content-split-image.php new file mode 100644 index 0000000..db6a2b4 --- /dev/null +++ b/patterns/content/content-split-image.php @@ -0,0 +1,67 @@ + __( 'Split Content with Image', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+ + +

We Build Solutions That Last

+ + + +

Our team combines creativity with technical expertise to deliver exceptional results that drive real business growth.

+ + + +
+
+

10+ years of industry experience

+
+ + + +
+

Award-winning design team

+
+ + + +
+

24/7 dedicated support

+
+ + + +
+

100% satisfaction guarantee

+
+
+ + + +
+
+ + + +
+
Team collaboration
+
+
+
+', +); diff --git a/patterns/content/content-split-reveal.php b/patterns/content/content-split-reveal.php new file mode 100644 index 0000000..e5e5ffc --- /dev/null +++ b/patterns/content/content-split-reveal.php @@ -0,0 +1,57 @@ + __( 'Content Split Reveal', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

About Us

+ + + +

We Build Digital Experiences That Matter

+ + + +

For over a decade, we have been helping businesses transform their digital presence. Our team of experts combines creativity with technical excellence to deliver solutions that drive real results.

+ + + +

We believe in building lasting partnerships with our clients, understanding their unique challenges, and crafting tailored solutions that exceed expectations.

+ + + +
+
0
Projects Delivered
+ + + +
0
Team Members
+ + + +
0
Years of Experience
+
+
+ + + +
+
Team collaboration
+
+
+
+', +); diff --git a/patterns/content/content-sponsor-logos.php b/patterns/content/content-sponsor-logos.php new file mode 100644 index 0000000..bec1f93 --- /dev/null +++ b/patterns/content/content-sponsor-logos.php @@ -0,0 +1,43 @@ + __( 'Sponsor Logos Bar', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+

Our Sponsors

+ + + +
+

TECHCORP

+ + + +

CLOUDIFY

+ + + +

DATAFLOW

+ + + +

NEXUSAI

+ + + +

SCALABLE

+
+
+', +); diff --git a/patterns/content/content-stats-counters.php b/patterns/content/content-stats-counters.php new file mode 100644 index 0000000..0f08ad9 --- /dev/null +++ b/patterns/content/content-stats-counters.php @@ -0,0 +1,69 @@ + __( 'Stats Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Our Impact in Numbers

+ + + +

Trusted by thousands of businesses worldwide

+
+ + + +
+
+ + +
+
0
Active Users
+
+
+ + + +
+ + +
+
0
Countries Served
+
+
+ + + +
+ + +
+
0
Average Rating
+
+
+ + + +
+ + +
+
0
YoY Growth
+
+
+
+
+', +); diff --git a/patterns/content/content-stats-dark-bar.php b/patterns/content/content-stats-dark-bar.php new file mode 100644 index 0000000..1a0c366 --- /dev/null +++ b/patterns/content/content-stats-dark-bar.php @@ -0,0 +1,35 @@ + __( 'Stats Counter Dark Bar', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
0
Happy Customers
+ + + +
0
Products
+ + + +
0
Countries
+ + + +
0
Rating
+
+
+', +); diff --git a/patterns/content/content-tabs-section.php b/patterns/content/content-tabs-section.php new file mode 100644 index 0000000..b551bff --- /dev/null +++ b/patterns/content/content-tabs-section.php @@ -0,0 +1,189 @@ + __( 'Content Tabs Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Explore Our Services

+ + + +

Discover how we can help you achieve your goals

+ + + +
+ + + + + + + + + + + + + +
+
+
+', +); diff --git a/patterns/content/content-team-stats.php b/patterns/content/content-team-stats.php new file mode 100644 index 0000000..176bee9 --- /dev/null +++ b/patterns/content/content-team-stats.php @@ -0,0 +1,57 @@ + __( 'Team Stats with Description', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Why Choose Us

+ + + +

Built for Teams Who Ship

+ + + +

We understand the challenges of modern product development. That is why we built a platform that gets out of your way and lets you focus on what matters.

+ + + +
+
0
Uptime SLA
+ + + +
0
Avg Response
+ + + +
0
Active Users
+ + + +
0
Support
+
+
+ + + +
+
Analytics dashboard
+
+
+
+', +); diff --git a/patterns/content/content-timeline.php b/patterns/content/content-timeline.php new file mode 100644 index 0000000..bdc16ac --- /dev/null +++ b/patterns/content/content-timeline.php @@ -0,0 +1,47 @@ + __( 'Timeline History', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+

Our Journey

+ + + +

Key milestones that shaped who we are today

+ + + +
+
2018

Company Founded

+

Started in a small garage with just two founders and a vision to transform the industry.

+
+ + + +
2020

Series A Funding

+

Raised $10M in Series A funding to accelerate product development and expand our team.

+
+ + + +
2023

Global Expansion

+

Opened offices in London, Tokyo, and Sydney, reaching customers in over 100 countries.

+
+
+
+
+', +); diff --git a/patterns/content/content-trust-companies.php b/patterns/content/content-trust-companies.php new file mode 100644 index 0000000..fd70af6 --- /dev/null +++ b/patterns/content/content-trust-companies.php @@ -0,0 +1,43 @@ + __( 'Trust Companies Bar', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+

Trusted by 10,000+ companies worldwide

+ + + +
+

Acme Corp

+ + + +

TechFlow

+ + + +

Innovate Inc

+ + + +

DataStream

+ + + +

CloudBase

+
+
+', +); diff --git a/patterns/content/content-why-choose-us.php b/patterns/content/content-why-choose-us.php new file mode 100644 index 0000000..b907dbb --- /dev/null +++ b/patterns/content/content-why-choose-us.php @@ -0,0 +1,93 @@ + __( 'Why Choose Us Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

Why Clients Choose Us

+ + + +

We combine deep expertise with personalized service to deliver exceptional outcomes for every client.

+ + + +
+
+ + +
+

Proven Track Record

+ + + +

Over 30 years of successful outcomes for clients across industries.

+
+
+ + + +
+ + +
+

Personalized Attention

+ + + +

Direct access to senior partners who personally handle your matters.

+
+
+ + + +
+ + +
+

Transparent Communication

+ + + +

Clear updates and honest advice throughout your engagement.

+
+
+ + + +
+ + +
+

Results-Driven Approach

+ + + +

Focused strategies designed to achieve your specific objectives.

+
+
+
+
+ + + +
+
Business handshake
+
+
+
+', +); diff --git a/patterns/content/content-with-toc.php b/patterns/content/content-with-toc.php new file mode 100644 index 0000000..d880b35 --- /dev/null +++ b/patterns/content/content-with-toc.php @@ -0,0 +1,113 @@ + __( 'Content with Table of Contents', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
+
+
+

On This Page

+ + + +
Table of Contents
    +
    + + + +
    +

    Introduction

    + + + +

    Welcome to our comprehensive guide. This article will walk you through everything you need to know to get started and make the most of our platform. Whether you are a beginner or an experienced user, you will find valuable insights here.

    + + + +

    We have designed this guide to be easy to follow, with clear explanations and practical examples that you can apply immediately to your projects.

    + + + +

    Getting Started

    + + + +

    The first step is to understand the fundamentals. Our platform is built on modern web standards and follows best practices for performance, accessibility, and user experience.

    + + + +

    To begin, you will need to familiarize yourself with the basic concepts and terminology. This foundation will help you navigate the more advanced features with confidence.

    + + + +

    Key Features

    + + + +

    Our platform offers a rich set of features designed to streamline your workflow and enhance your productivity. Here are some of the highlights:

    + + + +
      +
    • Intuitive drag-and-drop interface
    • + + + +
    • Real-time collaboration tools
    • + + + +
    • Extensive customization options
    • + + + +
    • Built-in analytics and reporting
    • + + + +
    • Seamless third-party integrations
    • +
    + + + +

    Best Practices

    + + + +

    To get the most out of our platform, we recommend following these best practices that have been refined through years of user feedback and research.

    + + + +

    First, start with a clear plan and objectives. Understanding what you want to achieve will help you make better decisions throughout your project.

    + + + +

    Second, take advantage of templates and patterns. These pre-built components can save you time and ensure consistency across your work.

    + + + +

    Conclusion

    + + + +

    We hope this guide has given you a solid understanding of how to use our platform effectively. Remember, the best way to learn is by doing, so do not be afraid to experiment and explore.

    + + + +

    If you have any questions or need further assistance, our support team is always here to help. Happy building!

    +
    +
    +
    +', +); diff --git a/patterns/content/icon-features.php b/patterns/content/icon-features.php new file mode 100644 index 0000000..ff38966 --- /dev/null +++ b/patterns/content/icon-features.php @@ -0,0 +1,83 @@ + __( 'Icon Features List', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Why Choose Us

    + + + +

    We combine cutting-edge technology with user-centered design to create experiences that delight and convert.

    +
    + + + +
    +
    +
    +

    Enterprise Security

    + + + +

    Bank-level encryption and security measures protect your data at every level.

    +
    + + + +
    +

    24/7 Support

    + + + +

    Our dedicated team is always available to help you succeed, day or night.

    +
    + + + +
    +

    Scalable Solutions

    + + + +

    Grow without limits. Our infrastructure scales seamlessly with your business.

    +
    + + + +
    +

    Developer Friendly

    + + + +

    Clean APIs, comprehensive docs, and developer tools make integration a breeze.

    +
    + + + +
    +

    Global CDN

    + + + +

    Lightning-fast performance worldwide with our global content delivery network.

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/content/progress-skills.php b/patterns/content/progress-skills.php new file mode 100644 index 0000000..4603f1d --- /dev/null +++ b/patterns/content/progress-skills.php @@ -0,0 +1,55 @@ + __( 'Progress Skills', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Our Expertise

    + + + +

    We have honed our skills over years of experience to deliver exceptional results in every project we undertake.

    + + + + +
    + + + +
    +
    WordPress Development - 95%
    + + + +
    React & JavaScript - 90%
    + + + +
    UI/UX Design - 85%
    + + + +
    Performance Optimization - 80%
    + + + +
    SEO & Accessibility - 88%
    +
    +
    +
    +', +); diff --git a/patterns/content/stats-counter.php b/patterns/content/stats-counter.php new file mode 100644 index 0000000..571bb09 --- /dev/null +++ b/patterns/content/stats-counter.php @@ -0,0 +1,45 @@ + __( 'Stats Counter', 'airo-wp' ), + 'categories' => array( 'airo-wp-content' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    By the Numbers

    + + + +

    See the impact we have made together

    +
    + + + + +
    +', +); diff --git a/patterns/cta/cta-banner-gradient.php b/patterns/cta/cta-banner-gradient.php new file mode 100644 index 0000000..7a0b9ca --- /dev/null +++ b/patterns/cta/cta-banner-gradient.php @@ -0,0 +1,41 @@ + __( 'CTA Banner with Gradient', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Ready to Transform Your Business?

    + + + +

    Join thousands of companies already using our platform to grow faster.

    +
    + + + +
    +
    + + + +
    +
    + + + +

    No credit card required • 14-day free trial • Cancel anytime

    +
    +', +); diff --git a/patterns/cta/cta-banner.php b/patterns/cta/cta-banner.php new file mode 100644 index 0000000..eecb90d --- /dev/null +++ b/patterns/cta/cta-banner.php @@ -0,0 +1,35 @@ + __( 'CTA Banner with Countdown', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Limited Time Offer!

    + + + +

    Get 50% off your first year. This exclusive deal ends soon.

    +
    + + + +
    +
    00
    Days
    00
    Hours
    00
    Min
    00
    Sec
    Offer has ended!
    +
    +
    +
    +', +); diff --git a/patterns/cta/cta-contact-split.php b/patterns/cta/cta-contact-split.php new file mode 100644 index 0000000..cdce631 --- /dev/null +++ b/patterns/cta/cta-contact-split.php @@ -0,0 +1,65 @@ + __( 'CTA with Contact Details', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Start a Project

    + + + +

    Ready to Bring Your Vision to Life?

    + + + +

    Let us discuss your project and explore how we can help you achieve your goals.

    + + + + +
    + + + +
    +
    +
    + + +

    hello@creativestudio.com

    +
    + + + +
    + + +

    +1 (555) 123-4567

    +
    + + + +
    + + +

    123 Design Street, Creative City

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/cta/cta-countdown.php b/patterns/cta/cta-countdown.php new file mode 100644 index 0000000..b42c0e1 --- /dev/null +++ b/patterns/cta/cta-countdown.php @@ -0,0 +1,45 @@ + __( 'CTA Countdown', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + + +

    Black Friday Sale - 50% Off

    + + + +

    Don\'t miss out on our biggest sale of the year. Offer ends soon!

    + + + +
    00
    Days
    00
    Hours
    00
    Min
    00
    Sec
    The countdown has ended!
    + + + + + + + +

    No credit card required. Cancel anytime.

    +
    +', +); diff --git a/patterns/cta/cta-donation.php b/patterns/cta/cta-donation.php new file mode 100644 index 0000000..af332a6 --- /dev/null +++ b/patterns/cta/cta-donation.php @@ -0,0 +1,33 @@ + __( 'Donation CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Every Gift Creates Ripples of Change

    + + + +

    Your donation today will transform lives for generations to come. Together, we can build a world where everyone has the opportunity to thrive.

    + + + + +
    +
    +', +); diff --git a/patterns/cta/cta-enrollment.php b/patterns/cta/cta-enrollment.php new file mode 100644 index 0000000..04f8c47 --- /dev/null +++ b/patterns/cta/cta-enrollment.php @@ -0,0 +1,37 @@ + __( 'Enrollment CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Start Your Learning Journey Today

    + + + +

    Join our community of learners and unlock your potential. Get unlimited access to all courses with our subscription plans.

    + + + + +
    +
    +', +); diff --git a/patterns/cta/cta-event-register.php b/patterns/cta/cta-event-register.php new file mode 100644 index 0000000..153e990 --- /dev/null +++ b/patterns/cta/cta-event-register.php @@ -0,0 +1,33 @@ + __( 'Event Registration CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Ready to Join the Future?

    + + + +

    Do not miss the most anticipated tech event of 2026. Limited seats available - secure yours today!

    + + + + +
    +
    +', +); diff --git a/patterns/cta/cta-get-started.php b/patterns/cta/cta-get-started.php new file mode 100644 index 0000000..18794cf --- /dev/null +++ b/patterns/cta/cta-get-started.php @@ -0,0 +1,39 @@ + __( 'Get Started CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +

    Ready to Get Started?

    + + + +

    Join thousands of teams already using our platform to work smarter.

    + + + + + + + +

    No credit card required. 14-day free trial.

    +
    +', +); diff --git a/patterns/cta/cta-gradient.php b/patterns/cta/cta-gradient.php new file mode 100644 index 0000000..dcf3851 --- /dev/null +++ b/patterns/cta/cta-gradient.php @@ -0,0 +1,37 @@ + __( 'CTA Gradient', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Ready to Get Started?

    + + + +

    Join thousands of satisfied customers who have transformed their business with our solutions.

    + + + + +
    +
    +', +); diff --git a/patterns/cta/cta-newsletter-subscribe.php b/patterns/cta/cta-newsletter-subscribe.php new file mode 100644 index 0000000..90aa2b1 --- /dev/null +++ b/patterns/cta/cta-newsletter-subscribe.php @@ -0,0 +1,37 @@ + __( 'Newsletter Subscribe CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Get 15% Off Your First Order

    + + + +

    Subscribe to our newsletter for exclusive deals, new arrivals, and style inspiration delivered straight to your inbox.

    + + + + + + + +

    No spam, unsubscribe anytime. By subscribing you agree to our Privacy Policy.

    +
    +
    +', +); diff --git a/patterns/cta/cta-newsletter.php b/patterns/cta/cta-newsletter.php new file mode 100644 index 0000000..df942fb --- /dev/null +++ b/patterns/cta/cta-newsletter.php @@ -0,0 +1,67 @@ + __( 'Newsletter Signup', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Stay in the Loop

    + + + +

    Get the latest updates, tips, and exclusive offers delivered straight to your inbox.

    + + + +
    +
    +

    Weekly insights and tutorials

    +
    + + + +
    +

    Early access to new features

    +
    + + + +
    +

    No spam, unsubscribe anytime

    +
    +
    +
    + + + +
    +
    +
    + + + + +
    + + + +

    Join 10,000+ subscribers

    +
    +
    +
    +', +); diff --git a/patterns/cta/cta-reservation.php b/patterns/cta/cta-reservation.php new file mode 100644 index 0000000..cfc1d63 --- /dev/null +++ b/patterns/cta/cta-reservation.php @@ -0,0 +1,33 @@ + __( 'Reservation CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Ready for an Unforgettable Evening?

    + + + +

    Reserve your table today and let us create a memorable dining experience for you and your guests.

    + + + + +
    +
    +', +); diff --git a/patterns/cta/cta-saas-transform.php b/patterns/cta/cta-saas-transform.php new file mode 100644 index 0000000..b51046e --- /dev/null +++ b/patterns/cta/cta-saas-transform.php @@ -0,0 +1,35 @@ + __( 'SaaS Transform CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +

    Ready to Transform Your Workflow?

    + + + +

    Join thousands of teams already using our platform to ship better products faster. Start your free trial today.

    + + + + +
    +', +); diff --git a/patterns/cta/cta-split.php b/patterns/cta/cta-split.php new file mode 100644 index 0000000..e969698 --- /dev/null +++ b/patterns/cta/cta-split.php @@ -0,0 +1,47 @@ + __( 'CTA Split with Blobs', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + + +
    +

    Ready to Launch Your Project?

    + + + +

    Join thousands of creators who have transformed their ideas into stunning websites. Start building today with our powerful tools.

    + + + + + + + +

    No credit card required • Free forever plan available

    +
    +
    +
    +', +); diff --git a/patterns/cta/cta-transformation.php b/patterns/cta/cta-transformation.php new file mode 100644 index 0000000..ed88771 --- /dev/null +++ b/patterns/cta/cta-transformation.php @@ -0,0 +1,33 @@ + __( 'Transformation CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Ready to Start Your Transformation?

    + + + +

    Join today and get your first week free. No commitment, no risk. Just results.

    + + + + +
    +
    +', +); diff --git a/patterns/cta/cta-volunteer-signup.php b/patterns/cta/cta-volunteer-signup.php new file mode 100644 index 0000000..c958649 --- /dev/null +++ b/patterns/cta/cta-volunteer-signup.php @@ -0,0 +1,79 @@ + __( 'Volunteer Signup CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Get Involved

    + + + +

    Ways to Make a Difference

    + + + +

    There are many ways to support our mission beyond financial donations. Your time, skills, and voice can make a tremendous impact.

    + + + +
    +
    +

    Join our volunteer network and make a hands-on difference. We offer both local and international volunteer opportunities, from community events to field missions.

    +
    + + + +
    + + + +
    + + + +
    +
    +
    + + + +
    +

    Sign Up to Volunteer

    + + + +
    +
    + + + + + + + +
    +
    +
    +
    +
    +', +); diff --git a/patterns/cta/cta-work-together.php b/patterns/cta/cta-work-together.php new file mode 100644 index 0000000..45773b3 --- /dev/null +++ b/patterns/cta/cta-work-together.php @@ -0,0 +1,59 @@ + __( 'Work Together CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-cta' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Get in Touch

    + + + +

    Let us Work Together

    + + + +

    Have a project in mind? I would love to hear about it. Drop me a message and let us create something amazing together.

    + + + + + + + +
    + + + + + + + + + + + + + +
    +
    +
    +', +); diff --git a/patterns/faq/accordion-faq.php b/patterns/faq/accordion-faq.php new file mode 100644 index 0000000..54a113f --- /dev/null +++ b/patterns/faq/accordion-faq.php @@ -0,0 +1,71 @@ + __( 'FAQ Accordion', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    FAQ

    + + + +

    Frequently Asked Questions

    + + + +

    Can\'t find what you\'re looking for? Feel free to contact our support team for personalized assistance.

    + + + + +
    + + + +
    +
    +
    +

    Getting started is easy! Simply create an account, choose your plan, and follow our quick setup wizard. You\'ll be up and running in less than 5 minutes.

    +
    + + + +
    + + + +
    + + + +
    + + + +
    +
    +
    +
    +
    +', +); diff --git a/patterns/faq/faq-accordion-animated.php b/patterns/faq/faq-accordion-animated.php new file mode 100644 index 0000000..e7318c8 --- /dev/null +++ b/patterns/faq/faq-accordion-animated.php @@ -0,0 +1,71 @@ + __( 'FAQ Accordion Animated', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    FAQ

    + + + +

    Frequently Asked Questions

    + + + +

    Can\'t find what you\'re looking for? Feel free to contact our support team for personalized assistance.

    + + + + +
    + + + +
    +
    +
    +

    Getting started is easy! Simply create an account, choose your plan, and follow our quick setup wizard. You\'ll be up and running in less than 5 minutes.

    +
    + + + +
    + + + +
    + + + +
    + + + +
    +
    +
    +
    +
    +', +); diff --git a/patterns/faq/faq-accordion-categories.php b/patterns/faq/faq-accordion-categories.php new file mode 100644 index 0000000..69c59c4 --- /dev/null +++ b/patterns/faq/faq-accordion-categories.php @@ -0,0 +1,145 @@ + __( 'FAQ Accordion with Categories', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Frequently Asked Questions

    + + + +

    Find answers to common questions about our services

    +
    + + + +
    +
    +
    +
    + + +

    General Questions

    +
    + + + +
    What services do you offer? +

    We offer a comprehensive range of digital services including web design, development, digital marketing, SEO optimization, and ongoing maintenance and support for your website.

    +
    + + + +
    How long does a typical project take? +

    Project timelines vary based on complexity. A simple website typically takes 2-4 weeks, while more complex projects with custom functionality may take 6-12 weeks or longer.

    +
    + + + +
    Do you work with clients remotely? +

    Yes! We work with clients worldwide through video calls, email, and project management tools. Distance is never an obstacle to delivering exceptional results.

    +
    +
    + + + +
    +
    + + +

    Pricing & Payment

    +
    + + + +
    What are your payment terms? +

    We typically require 50% deposit upfront with the remaining balance due upon project completion. For larger projects, we can arrange milestone-based payments.

    +
    + + + +
    Do you offer refunds? +

    We offer a satisfaction guarantee. If you are not happy with our work, we will revise it until you are satisfied. Refunds are available within the first week if no substantial work has begun.

    +
    + + + +
    What payment methods do you accept? +

    We accept all major credit cards, PayPal, bank transfers, and can accommodate other payment methods upon request for international clients.

    +
    +
    +
    + + + +
    +
    +
    + + +

    Technical Support

    +
    + + + +
    Do you provide ongoing support? +

    Yes, we offer various maintenance and support packages to keep your website secure, updated, and running smoothly after launch.

    +
    + + + +
    Will I be able to update my website myself? +

    Absolutely! We build on WordPress with user-friendly interfaces. We also provide training so you can confidently manage your content and make updates.

    +
    + + + +
    What if something breaks on my site? +

    With our support packages, we monitor your site and address issues quickly. We also maintain regular backups so your data is always protected.

    +
    +
    + + + +
    +
    + + +

    Security & Privacy

    +
    + + + +
    How do you ensure website security? +

    We implement industry-standard security measures including SSL certificates, regular updates, secure hosting, firewalls, and malware scanning to protect your site.

    +
    + + + +
    Is my data kept confidential? +

    Yes, we take confidentiality seriously. All client data is protected under our privacy policy, and we can sign NDAs for sensitive projects upon request.

    +
    + + + +
    Do you comply with GDPR? +

    Yes, we ensure all websites we build are GDPR compliant with proper cookie notices, privacy policies, and data handling procedures in place.

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/faq/faq-professional.php b/patterns/faq/faq-professional.php new file mode 100644 index 0000000..99033d4 --- /dev/null +++ b/patterns/faq/faq-professional.php @@ -0,0 +1,49 @@ + __( 'Professional FAQ Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Frequently Asked Questions

    +
    + + + +
    +
    +

    Please bring any relevant documents related to your matter, such as contracts, correspondence, or financial statements. A written summary of your situation and questions you would like answered is also helpful.

    +
    + + + +
    + + + +
    + + + +
    +
    +
    +', +); diff --git a/patterns/faq/faq-saas.php b/patterns/faq/faq-saas.php new file mode 100644 index 0000000..1116ed3 --- /dev/null +++ b/patterns/faq/faq-saas.php @@ -0,0 +1,59 @@ + __( 'SaaS FAQ Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Frequently Asked Questions

    + + + +

    Everything you need to know about our platform.

    +
    + + + +
    +
    + + + +
    + + + +
    +

    We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for annual enterprise plans. All payments are processed securely through Stripe.

    +
    + + + +
    + + + +
    +
    +
    +', +); diff --git a/patterns/faq/faq-split-image.php b/patterns/faq/faq-split-image.php new file mode 100644 index 0000000..c20895d --- /dev/null +++ b/patterns/faq/faq-split-image.php @@ -0,0 +1,65 @@ + __( 'FAQ Split with Image', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    FAQ

    + + + +

    Frequently Asked Questions

    + + + +

    Find answers to common questions about our products, shipping, and return policies.

    + + + +
    +
    +

    Standard shipping takes 3-5 business days within the US. Express shipping (1-2 days) is available for an additional fee. International orders typically arrive within 7-14 business days.

    +
    + + + +
    + + + +
    + + + +
    +
    +
    + + + +
    +
    Shopping experience
    +
    +
    +
    +', +); diff --git a/patterns/faq/faq-tabs.php b/patterns/faq/faq-tabs.php new file mode 100644 index 0000000..ac037ff --- /dev/null +++ b/patterns/faq/faq-tabs.php @@ -0,0 +1,95 @@ + __( 'FAQ with Tabs', 'airo-wp' ), + 'categories' => array( 'airo-wp-faq' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Frequently Asked Questions

    + + + +

    Find answers organized by topic

    +
    + + + +
    + + + + + + + + + +
    +
    +', +); diff --git a/patterns/features/features-accordion.php b/patterns/features/features-accordion.php new file mode 100644 index 0000000..1aa0510 --- /dev/null +++ b/patterns/features/features-accordion.php @@ -0,0 +1,59 @@ + __( 'Features Accordion', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Dive Deeper Into Our Features

    + + + +

    Explore the powerful capabilities that make our platform the choice of professionals worldwide.

    + + + + +
    + + + +
    +
    + + + +
    +

    Engage your visitors with tabs, accordions, sliders, and modals. All components are fully accessible and work seamlessly across devices with smooth animations.

    +
    + + + +
    + + + +
    +
    +
    +
    +', +); diff --git a/patterns/features/features-animated-cards.php b/patterns/features/features-animated-cards.php new file mode 100644 index 0000000..2a0b494 --- /dev/null +++ b/patterns/features/features-animated-cards.php @@ -0,0 +1,105 @@ + __( 'Features Animated Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Features

    + + + +

    Everything You Need

    + + + +

    Powerful features to help you build better products faster.

    +
    + + + +
    +
    + + +

    Lightning Fast

    + + + +

    Optimized for speed with lazy loading and efficient code that keeps your site running smoothly.

    +
    + + + +
    + + +

    Secure by Default

    + + + +

    Built with security best practices to keep your data safe and your site protected.

    +
    + + + +
    + + +

    Fully Customizable

    + + + +

    Tailor every aspect to match your brand with extensive customization options.

    +
    + + + +
    + + +

    Analytics Built-in

    + + + +

    Track your performance with built-in analytics and detailed reporting dashboards.

    +
    + + + +
    + + +

    Team Collaboration

    + + + +

    Work together seamlessly with team features, permissions, and real-time collaboration.

    +
    + + + +
    + + +

    24/7 Support

    + + + +

    Get help when you need it with our dedicated support team available around the clock.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-benefit-cards.php b/patterns/features/features-benefit-cards.php new file mode 100644 index 0000000..bcaa047 --- /dev/null +++ b/patterns/features/features-benefit-cards.php @@ -0,0 +1,55 @@ + __( 'Benefit Icon Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Free Shipping

    + + + +

    Free shipping on all orders over $50. Fast delivery within 3-5 business days.

    +
    + + + +
    + + +

    Easy Returns

    + + + +

    30-day hassle-free returns. Not satisfied? Get a full refund, no questions asked.

    +
    + + + +
    + + +

    Secure Payment

    + + + +

    Your payment information is protected with industry-leading encryption.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-class-types.php b/patterns/features/features-class-types.php new file mode 100644 index 0000000..5d8d4de --- /dev/null +++ b/patterns/features/features-class-types.php @@ -0,0 +1,103 @@ + __( 'Fitness Class Types', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Classes

    + + + +

    Train Your Way

    + + + +

    From high-intensity workouts to mindful yoga, we offer classes for every fitness level and goal.

    +
    + + + +
    +
    + + +

    HIIT Training

    + + + +

    High-intensity interval training to maximize calorie burn and boost metabolism.

    + + + +

    45 min • All Levels

    +
    + + + +
    + + +

    Strength Training

    + + + +

    Build muscle and increase strength with our expert-led weight training sessions.

    + + + +

    60 min • Intermediate

    +
    + + + +
    + + +

    Yoga & Pilates

    + + + +

    Improve flexibility, balance, and mental clarity with mindful movement practices.

    + + + +

    60 min • All Levels

    +
    + + + +
    + + +

    Group Cycling

    + + + +

    High-energy indoor cycling with motivating music and inspiring instructors.

    + + + +

    45 min • All Levels

    +
    +
    + + + + +
    +', +); diff --git a/patterns/features/features-flip-card-grid.php b/patterns/features/features-flip-card-grid.php new file mode 100644 index 0000000..72791e8 --- /dev/null +++ b/patterns/features/features-flip-card-grid.php @@ -0,0 +1,117 @@ + __( 'Features Flip Card Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Features

    + + + +

    Everything You Need to Scale

    + + + +

    Powerful tools designed to help your team move faster and build better products.

    +
    + + + +
    +
    +
    + + +

    Real-time Analytics

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Real-time Analytics

    + + +

    Track every metric that matters with live dashboards. Get instant insights into user behavior, conversion rates, and performance metrics.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Team Collaboration

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Team Collaboration

    + + +

    Work together seamlessly with real-time editing, comments, and shared workspaces. Keep everyone aligned and moving fast.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Automation

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Automation

    + + +

    Automate repetitive tasks and workflows. Set up triggers, actions, and rules that work while you sleep.

    + + +

    Learn more →

    + +
    +
    +
    +
    +', +); diff --git a/patterns/features/features-flip-card-services.php b/patterns/features/features-flip-card-services.php new file mode 100644 index 0000000..e72d3bf --- /dev/null +++ b/patterns/features/features-flip-card-services.php @@ -0,0 +1,113 @@ + __( 'Flip Card Services', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Services

    + + + +

    White-Glove Service at Every Step

    +
    + + + +
    +
    +
    + + +

    Buyer Representation

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Buyer Representation

    + + +

    Our expert agents guide you through every step of finding and acquiring your perfect property. From initial search to closing, we negotiate on your behalf to secure the best terms.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Property Valuation

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Property Valuation

    + + +

    Receive accurate, data-driven valuations backed by our deep knowledge of luxury markets. We analyze comparable sales, market trends, and unique property features.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Global Reach

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Global Reach

    + + +

    Access exclusive listings worldwide through our international network. From New York penthouses to Mediterranean villas, we connect you with extraordinary properties globally.

    + + +

    Learn more →

    + +
    +
    +
    +
    +', +); diff --git a/patterns/features/features-focus-areas.php b/patterns/features/features-focus-areas.php new file mode 100644 index 0000000..5f4a509 --- /dev/null +++ b/patterns/features/features-focus-areas.php @@ -0,0 +1,77 @@ + __( 'Focus Areas Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Focus Areas

    + + + +

    Where Your Support Goes

    +
    + + + +
    +
    + + +

    Education

    + + + +

    Building schools and providing scholarships to give children the education they deserve.

    +
    + + + +
    + + +

    Clean Water

    + + + +

    Installing wells and water systems to provide safe drinking water to communities.

    +
    + + + +
    + + +

    Healthcare

    + + + +

    Funding medical clinics and providing essential healthcare services to underserved areas.

    +
    + + + +
    + + +

    Economic Empowerment

    + + + +

    Microloans and vocational training to help families build sustainable livelihoods.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-grid-icons.php b/patterns/features/features-grid-icons.php new file mode 100644 index 0000000..a07269b --- /dev/null +++ b/patterns/features/features-grid-icons.php @@ -0,0 +1,113 @@ + __( 'Feature Grid with Icons', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Everything You Need

    + + + +

    Powerful features to help you succeed

    +
    + + + +
    +
    +
    + + + +

    Lightning Fast

    + + + +

    Optimized for speed with sub-second load times and efficient resource usage across all devices.

    +
    + + + +
    +
    + + + +

    Secure by Default

    + + + +

    Enterprise-grade security with encryption, regular audits, and compliance with industry standards.

    +
    + + + +
    +
    + + + +

    Fully Customizable

    + + + +

    Tailor every aspect to match your brand with extensive customization options and flexible settings.

    +
    + + + +
    +
    + + + +

    Auto Updates

    + + + +

    Stay current with automatic updates that bring new features and security patches seamlessly.

    +
    + + + +
    +
    + + + +

    24/7 Support

    + + + +

    Round-the-clock expert support to help you whenever you need assistance with any issues.

    +
    + + + +
    +
    + + + +

    Analytics Built-in

    + + + +

    Comprehensive analytics dashboard to track performance and make data-driven decisions.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-grid.php b/patterns/features/features-grid.php new file mode 100644 index 0000000..c0b0919 --- /dev/null +++ b/patterns/features/features-grid.php @@ -0,0 +1,53 @@ + __( 'Features Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Everything You Need

    + + + +

    Powerful features to help you build better websites, faster.

    +
    + + + +
    +

    Lightning Fast

    Optimized for performance with lazy loading, minimal CSS, and efficient JavaScript.

    + + + +

    Fully Responsive

    Every block adapts beautifully to any screen size, from mobile to desktop.

    + + + +

    Accessible

    Built with WCAG guidelines in mind, ensuring your site is usable by everyone.

    + + + +

    Easy to Customize

    Adjust colors, spacing, typography, and more using the familiar WordPress interface.

    + + + +

    Theme Compatible

    Works seamlessly with any WordPress theme that supports the block editor.

    + + + +

    Regular Updates

    Constantly improved with new features, bug fixes, and WordPress compatibility.

    +
    +
    +', +); diff --git a/patterns/features/features-learning-paths.php b/patterns/features/features-learning-paths.php new file mode 100644 index 0000000..be4bf6b --- /dev/null +++ b/patterns/features/features-learning-paths.php @@ -0,0 +1,81 @@ + __( 'Learning Path Categories', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Popular Categories

    + + + +

    Explore Learning Paths

    + + + +

    Choose from a wide range of categories to find the perfect course for your career goals.

    +
    + + + +
    +
    + + +

    Web Development

    + + + +

    120+ Courses

    +
    + + + +
    + + +

    Data Science

    + + + +

    85+ Courses

    +
    + + + +
    + + +

    Design & UX

    + + + +

    95+ Courses

    +
    + + + +
    + + +

    Business & Marketing

    + + + +

    200+ Courses

    +
    +
    +
    +', +); diff --git a/patterns/features/features-numbered-services.php b/patterns/features/features-numbered-services.php new file mode 100644 index 0000000..1b19635 --- /dev/null +++ b/patterns/features/features-numbered-services.php @@ -0,0 +1,89 @@ + __( 'Numbered Services with Counters', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    What We Do

    + + + +

    Services That Drive Growth

    + + + +

    From strategy to execution, we help ambitious brands stand out in a crowded marketplace.

    +
    + + + +
    +
    +
    0
    Projects Completed
    + + + +
    0
    Industry Awards
    +
    +
    +
    + + + +
    +
    +

    01

    + + + +

    Brand Strategy

    + + + +

    We craft compelling brand identities that resonate with your audience and differentiate you from competitors.

    +
    + + + +
    +

    02

    + + + +

    Web Design

    + + + +

    Beautiful, functional websites that convert visitors into customers and showcase your brand at its best.

    +
    + + + +
    +

    03

    + + + +

    Digital Marketing

    + + + +

    Data-driven campaigns that reach your target audience and deliver measurable business results.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-portfolio-services.php b/patterns/features/features-portfolio-services.php new file mode 100644 index 0000000..84f3cdd --- /dev/null +++ b/patterns/features/features-portfolio-services.php @@ -0,0 +1,65 @@ + __( 'Portfolio Services Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    What I Offer

    + + + +

    Services

    +
    + + + +
    +
    + + +

    Product Design

    + + + +

    End-to-end product design from concept to launch, including user research, wireframing, and high-fidelity prototypes.

    +
    + + + +
    + + +

    UI/UX Design

    + + + +

    Intuitive interfaces and seamless experiences that delight users and achieve business goals.

    +
    + + + +
    + + +

    Brand Identity

    + + + +

    Visual identity systems including logos, color palettes, typography, and brand guidelines.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-practice-areas.php b/patterns/features/features-practice-areas.php new file mode 100644 index 0000000..82ea3dc --- /dev/null +++ b/patterns/features/features-practice-areas.php @@ -0,0 +1,125 @@ + __( 'Practice Areas Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Practice Areas

    + + + +

    Comprehensive expertise across key areas to serve your needs.

    +
    + + + +
    +
    + + +

    Business Law

    + + + +

    Corporate formation, contracts, mergers and acquisitions, and business dispute resolution.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Real Estate

    + + + +

    Commercial and residential transactions, zoning, development, and property disputes.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Litigation

    + + + +

    Civil litigation, dispute resolution, arbitration, and aggressive courtroom representation.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Employment Law

    + + + +

    Workplace compliance, discrimination claims, employment contracts, and HR consulting.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Estate Planning

    + + + +

    Wills, trusts, estate administration, and wealth transfer strategies.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Tax Advisory

    + + + +

    Tax planning, IRS representation, and strategic tax minimization for individuals and businesses.

    + + + +

    Learn more

    +
    +
    +
    +', +); diff --git a/patterns/features/features-saas-grid.php b/patterns/features/features-saas-grid.php new file mode 100644 index 0000000..47b25fc --- /dev/null +++ b/patterns/features/features-saas-grid.php @@ -0,0 +1,101 @@ + __( 'SaaS Features Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Everything You Need to Succeed

    + + + +

    Powerful features designed to help you work smarter, not harder.

    +
    + + + +
    +
    + + +

    Lightning Fast

    + + + +

    Optimized performance that keeps your team moving fast without slowdowns.

    +
    + + + +
    + + +

    Enterprise Security

    + + + +

    Bank-level encryption and compliance certifications to protect your data.

    +
    + + + +
    + + +

    Team Collaboration

    + + + +

    Real-time collaboration tools that keep your entire team in sync.

    +
    + + + +
    + + +

    Advanced Analytics

    + + + +

    Actionable insights and custom dashboards to track what matters.

    +
    + + + +
    + + +

    Workflow Automation

    + + + +

    Automate repetitive tasks and focus on what really matters.

    +
    + + + +
    + + +

    Global CDN

    + + + +

    Lightning-fast delivery from data centers around the world.

    +
    +
    +
    +', +); diff --git a/patterns/features/features-tabs.php b/patterns/features/features-tabs.php new file mode 100644 index 0000000..32122b5 --- /dev/null +++ b/patterns/features/features-tabs.php @@ -0,0 +1,161 @@ + __( 'Features Tabs', 'airo-wp' ), + 'categories' => array( 'airo-wp-features' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Compare Features

    + + + +

    Choose the right plan for your needs

    +
    + + + +
    + + + + + + + + + +
    +
    +', +); diff --git a/patterns/footer/footer-centered.php b/patterns/footer/footer-centered.php new file mode 100644 index 0000000..413edf9 --- /dev/null +++ b/patterns/footer/footer-centered.php @@ -0,0 +1,29 @@ + __( 'Footer Centered', 'airo-wp' ), + 'categories' => array( 'airo-wp-footer' ), + 'blockTypes' => array( 'core/template-part/footer' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    + +
    + + + +

    © 2025 Your Company. All rights reserved.

    +
    +', +); diff --git a/patterns/footer/footer-classic.php b/patterns/footer/footer-classic.php new file mode 100644 index 0000000..b640a86 --- /dev/null +++ b/patterns/footer/footer-classic.php @@ -0,0 +1,39 @@ + __( 'Footer Classic', 'airo-wp' ), + 'categories' => array( 'airo-wp-footer' ), + 'blockTypes' => array( 'core/template-part/footer' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    + + + + + + + + +

    © 2025 Your Company. All rights reserved.

    +
    +
    +', +); diff --git a/patterns/footer/footer-columns.php b/patterns/footer/footer-columns.php new file mode 100644 index 0000000..47ea9fc --- /dev/null +++ b/patterns/footer/footer-columns.php @@ -0,0 +1,67 @@ + __( 'Footer Columns', 'airo-wp' ), + 'categories' => array( 'airo-wp-footer' ), + 'blockTypes' => array( 'core/template-part/footer' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Building better digital experiences for small businesses everywhere.

    + + + + +
    + + + +
    +
    Company
    + + +
    + + + +
    +
    Services
    + + +
    + + + +
    +
    Resources
    + + +
    +
    + + + +

    © 2025 Your Company. All rights reserved.

    +
    +', +); diff --git a/patterns/footer/footer-dark.php b/patterns/footer/footer-dark.php new file mode 100644 index 0000000..2b3ff5f --- /dev/null +++ b/patterns/footer/footer-dark.php @@ -0,0 +1,47 @@ + __( 'Footer Dark', 'airo-wp' ), + 'categories' => array( 'airo-wp-footer' ), + 'blockTypes' => array( 'core/template-part/footer' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    + + +
    + + + +
    +
    + + + +
    +

    © 2025 Your Company. All rights reserved.

    + + + + +
    +
    +', +); diff --git a/patterns/footer/footer-minimal.php b/patterns/footer/footer-minimal.php new file mode 100644 index 0000000..be59cf4 --- /dev/null +++ b/patterns/footer/footer-minimal.php @@ -0,0 +1,29 @@ + __( 'Footer Minimal', 'airo-wp' ), + 'categories' => array( 'airo-wp-footer' ), + 'blockTypes' => array( 'core/template-part/footer' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    © 2025 Your Company. All rights reserved.

    + + +
    +
    +
    +', +); diff --git a/patterns/gallery/gallery-atmosphere.php b/patterns/gallery/gallery-atmosphere.php new file mode 100644 index 0000000..24ab1dd --- /dev/null +++ b/patterns/gallery/gallery-atmosphere.php @@ -0,0 +1,45 @@ + __( 'Restaurant Atmosphere Gallery', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    The Experience

    + + + +

    Our Atmosphere

    +
    + + + +
    +
    Restaurant interior 1
    + + + +
    Restaurant interior 2
    + + + +
    Restaurant interior 3
    + + + +
    Restaurant interior 4
    +
    +
    +', +); diff --git a/patterns/gallery/gallery-grid.php b/patterns/gallery/gallery-grid.php new file mode 100644 index 0000000..158b882 --- /dev/null +++ b/patterns/gallery/gallery-grid.php @@ -0,0 +1,53 @@ + __( 'Gallery Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Work

    + + + +

    A showcase of our recent projects and creative work

    +
    + + + + +
    +', +); diff --git a/patterns/gallery/gallery-marquee.php b/patterns/gallery/gallery-marquee.php new file mode 100644 index 0000000..377d93d --- /dev/null +++ b/patterns/gallery/gallery-marquee.php @@ -0,0 +1,27 @@ + __( 'Gallery Marquee', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Trusted By Industry Leaders

    +
    + + + +
    Example image 1Example image 2Example image 3Example image 4
    Example image 1Example image 2Example image 3Example image 4
    Example image 1Example image 2Example image 3Example image 4
    Example image 1Example image 2Example image 3Example image 4
    Example image 1Example image 2Example image 3Example image 4
    Example image 1Example image 2Example image 3Example image 4
    Example image 5Example image 6Example image 7Example image 8
    Example image 5Example image 6Example image 7Example image 8
    Example image 5Example image 6Example image 7Example image 8
    Example image 5Example image 6Example image 7Example image 8
    Example image 5Example image 6Example image 7Example image 8
    Example image 5Example image 6Example image 7Example image 8
    +
    +', +); diff --git a/patterns/gallery/gallery-parallax-grid.php b/patterns/gallery/gallery-parallax-grid.php new file mode 100644 index 0000000..40ace42 --- /dev/null +++ b/patterns/gallery/gallery-parallax-grid.php @@ -0,0 +1,117 @@ + __( 'Gallery Parallax Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Work

    + + + +

    Featured Projects

    + + + +

    Explore our portfolio of innovative digital solutions.

    +
    + + + +
    +
    +
    Project showcase
    + + + +

    Brand Identity

    + + + +

    Digital Marketing

    +
    + + + +
    +
    Project showcase
    + + + +

    Analytics Dashboard

    + + + +

    Web Application

    +
    + + + +
    +
    Project showcase
    + + + +

    E-commerce Platform

    + + + +

    Online Store

    +
    + + + +
    +
    Project showcase
    + + + +

    Mobile App Design

    + + + +

    iOS & Android

    +
    + + + +
    +
    Project showcase
    + + + +

    SaaS Platform

    + + + +

    Enterprise Software

    +
    + + + +
    +
    Project showcase
    + + + +

    Corporate Website

    + + + +

    Business Solution

    +
    +
    +
    +', +); diff --git a/patterns/gallery/gallery-portfolio-projects.php b/patterns/gallery/gallery-portfolio-projects.php new file mode 100644 index 0000000..3e5d6f4 --- /dev/null +++ b/patterns/gallery/gallery-portfolio-projects.php @@ -0,0 +1,93 @@ + __( 'Portfolio Projects Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +
    +

    Selected Work

    + + + +

    Featured Projects

    +
    + + + + +
    +
    + + + +
    +
    +
    Project One
    + + + +

    Luxe Brand Redesign

    + + + +

    Branding / Web Design

    +
    + + + +
    +
    Project Two
    + + + +

    TechStart App Launch

    + + + +

    Digital Marketing / Strategy

    +
    + + + +
    +
    Project Three
    + + + +

    Eco Fashion Campaign

    + + + +

    Social Media / Content

    +
    + + + +
    +
    Project Four
    + + + +

    FinServ Platform

    + + + +

    UX Design / Development

    +
    +
    +
    +', +); diff --git a/patterns/gallery/gallery-property-showcase.php b/patterns/gallery/gallery-property-showcase.php new file mode 100644 index 0000000..3bd1143 --- /dev/null +++ b/patterns/gallery/gallery-property-showcase.php @@ -0,0 +1,91 @@ + __( 'Property Image Accordion', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Featured Listings

    + + + +

    Exceptional Properties Await

    +
    + + + +
    +
    +

    Beverly Hills

    + + + +

    Modern Estate

    + + + +

    $12,500,000 | 6 Bed | 8 Bath

    +
    + + + +
    +

    Malibu

    + + + +

    Oceanfront Villa

    + + + +

    $28,000,000 | 5 Bed | 7 Bath

    +
    + + + +
    +

    Bel Air

    + + + +

    Contemporary Mansion

    + + + +

    $45,000,000 | 8 Bed | 12 Bath

    +
    + + + +
    +

    Hollywood Hills

    + + + +

    Architectural Masterpiece

    + + + +

    $18,900,000 | 5 Bed | 6 Bath

    +
    +
    + + + + +
    +', +); diff --git a/patterns/gallery/gallery-selected-work.php b/patterns/gallery/gallery-selected-work.php new file mode 100644 index 0000000..1d6b386 --- /dev/null +++ b/patterns/gallery/gallery-selected-work.php @@ -0,0 +1,93 @@ + __( 'Selected Work Portfolio', 'airo-wp' ), + 'categories' => array( 'airo-wp-gallery' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +
    +

    Portfolio

    + + + +

    Selected Work

    +
    + + + + +
    +
    + + + +
    +
    +
    Mobile App Design
    + + + +

    FinTech Mobile App

    + + + +

    Product Design / Mobile App

    +
    + + + +
    +
    Dashboard Design
    + + + +

    SaaS Dashboard Redesign

    + + + +

    UX Design / Web App

    +
    + + + +
    +
    E-commerce Design
    + + + +

    E-commerce Experience

    + + + +

    UI Design / E-commerce

    +
    + + + +
    +
    Brand Identity
    + + + +

    Startup Brand Identity

    + + + +

    Branding / Visual Identity

    +
    +
    +
    +', +); diff --git a/patterns/header/header-centered.php b/patterns/header/header-centered.php new file mode 100644 index 0000000..613c7e1 --- /dev/null +++ b/patterns/header/header-centered.php @@ -0,0 +1,25 @@ + __( 'Header Centered', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    + +
    +
    +', +); diff --git a/patterns/header/header-classic-overlay.php b/patterns/header/header-classic-overlay.php new file mode 100644 index 0000000..69898f4 --- /dev/null +++ b/patterns/header/header-classic-overlay.php @@ -0,0 +1,27 @@ + __( 'Header Classic Overlay', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + +
    +', +); diff --git a/patterns/header/header-classic.php b/patterns/header/header-classic.php new file mode 100644 index 0000000..209b6df --- /dev/null +++ b/patterns/header/header-classic.php @@ -0,0 +1,27 @@ + __( 'Header Classic', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + +
    +
    +
    +', +); diff --git a/patterns/header/header-cta-overlay.php b/patterns/header/header-cta-overlay.php new file mode 100644 index 0000000..0878354 --- /dev/null +++ b/patterns/header/header-cta-overlay.php @@ -0,0 +1,33 @@ + __( 'Header CTA Overlay', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + +
    +', +); diff --git a/patterns/header/header-cta.php b/patterns/header/header-cta.php new file mode 100644 index 0000000..fece71f --- /dev/null +++ b/patterns/header/header-cta.php @@ -0,0 +1,33 @@ + __( 'Header with CTA', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +
    + + + +
    +
    +
    +
    +', +); diff --git a/patterns/header/header-dark-overlay.php b/patterns/header/header-dark-overlay.php new file mode 100644 index 0000000..5b4cce6 --- /dev/null +++ b/patterns/header/header-dark-overlay.php @@ -0,0 +1,33 @@ + __( 'Header Dark Overlay', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + +
    +', +); diff --git a/patterns/header/header-dark.php b/patterns/header/header-dark.php new file mode 100644 index 0000000..14c7cb1 --- /dev/null +++ b/patterns/header/header-dark.php @@ -0,0 +1,33 @@ + __( 'Header Dark', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + +
    +', +); diff --git a/patterns/header/header-topbar.php b/patterns/header/header-topbar.php new file mode 100644 index 0000000..ba918f9 --- /dev/null +++ b/patterns/header/header-topbar.php @@ -0,0 +1,51 @@ + __( 'Header with Topbar', 'airo-wp' ), + 'categories' => array( 'airo-wp-header' ), + 'blockTypes' => array( 'core/template-part/header' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    info@yourbusiness.com  ·  (555) 123-4567

    + + + + +
    +
    + + + +
    +
    + + +
    + + + +
    +
    +
    +
    +', +); diff --git a/patterns/headings/bold-plus-light.php b/patterns/headings/bold-plus-light.php new file mode 100644 index 0000000..ed6eb87 --- /dev/null +++ b/patterns/headings/bold-plus-light.php @@ -0,0 +1,17 @@ + __( 'Bold + Light Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    Buildsomething amazing.

    ', +); diff --git a/patterns/headings/color-pop.php b/patterns/headings/color-pop.php new file mode 100644 index 0000000..9034585 --- /dev/null +++ b/patterns/headings/color-pop.php @@ -0,0 +1,17 @@ + __( 'Color Pop Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    We makewebsitesthat work.

    ', +); diff --git a/patterns/headings/gradient-accent.php b/patterns/headings/gradient-accent.php new file mode 100644 index 0000000..800bb79 --- /dev/null +++ b/patterns/headings/gradient-accent.php @@ -0,0 +1,17 @@ + __( 'Gradient Accent Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    Transform yourcreative vision

    ', +); diff --git a/patterns/headings/hero-centered-heading.php b/patterns/headings/hero-centered-heading.php new file mode 100644 index 0000000..e3349e8 --- /dev/null +++ b/patterns/headings/hero-centered-heading.php @@ -0,0 +1,19 @@ + __( 'Hero Centered Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    Design.Build.Launch.

    Everything you need to create stunning websites that convert visitors into customers.

    ', +); diff --git a/patterns/headings/hero-split-heading.php b/patterns/headings/hero-split-heading.php new file mode 100644 index 0000000..700b106 --- /dev/null +++ b/patterns/headings/hero-split-heading.php @@ -0,0 +1,21 @@ + __( 'Hero Split Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    You need apowerful website

    Launch faster with beautiful, conversion-focused designs that adapt to any brand and speak to your audience.

    ', +); diff --git a/patterns/headings/highlighted-word.php b/patterns/headings/highlighted-word.php new file mode 100644 index 0000000..2c40b8f --- /dev/null +++ b/patterns/headings/highlighted-word.php @@ -0,0 +1,17 @@ + __( 'Highlighted Word Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    Your nextbig ideastarts here.

    ', +); diff --git a/patterns/headings/section-label-title.php b/patterns/headings/section-label-title.php new file mode 100644 index 0000000..e252355 --- /dev/null +++ b/patterns/headings/section-label-title.php @@ -0,0 +1,17 @@ + __( 'Section Label + Title', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    Our ServicesWhat we do best

    We combine strategy, design, and technology to create digital experiences that drive real results for your business.

    ', +); diff --git a/patterns/headings/uppercase-plus-italic.php b/patterns/headings/uppercase-plus-italic.php new file mode 100644 index 0000000..f777f9d --- /dev/null +++ b/patterns/headings/uppercase-plus-italic.php @@ -0,0 +1,17 @@ + __( 'Uppercase + Italic Heading', 'airo-wp' ), + 'categories' => array( 'airo-wp-headings' ), + 'viewportWidth' => 1200, + 'content' => '

    The Modernapproach

    ', +); diff --git a/patterns/hero/hero-agency-dark.php b/patterns/hero/hero-agency-dark.php new file mode 100644 index 0000000..0cca67b --- /dev/null +++ b/patterns/hero/hero-agency-dark.php @@ -0,0 +1,49 @@ + __( 'Agency Dark Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Award-Winning Creative Studio

    + + + +

    We Create
    Digital Experiences
    That Matter

    + + + +

    A full-service creative agency specializing in brand strategy, web design, and digital marketing that drives real results.

    + + + + +
    + + + +
    +
    Creative design work
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-bento.php b/patterns/hero/hero-bento.php new file mode 100644 index 0000000..88a5b75 --- /dev/null +++ b/patterns/hero/hero-bento.php @@ -0,0 +1,19 @@ + __( 'Hero Bento', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => '

    Strategic growth solutions built for ambitious startups.

    We partner with founders and teams to unlock potential, streamline operations, and accelerate sustainable business growth.

    + +
    Modern collaborative workspace with warm wood accents and natural light
    ', +); diff --git a/patterns/hero/hero-centered.php b/patterns/hero/hero-centered.php new file mode 100644 index 0000000..5c63bfe --- /dev/null +++ b/patterns/hero/hero-centered.php @@ -0,0 +1,33 @@ + __( 'Hero Centered', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Build Something Amazing

    + + + +

    Create stunning websites with powerful blocks designed for modern WordPress.

    + + + + +
    +
    +
    +', +); diff --git a/patterns/hero/hero-double-image.php b/patterns/hero/hero-double-image.php new file mode 100644 index 0000000..8b8085c --- /dev/null +++ b/patterns/hero/hero-double-image.php @@ -0,0 +1,19 @@ + __( 'Hero Double Image', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => '

    Elevate Your Space

    Thoughtfully designed pieces that transform everyday moments into extraordinary experiences

    Modern living room with elegant furniture and warm natural lighting
    Curated home accessories and decor with minimalist styling
    ', +); diff --git a/patterns/hero/hero-ecommerce-store.php b/patterns/hero/hero-ecommerce-store.php new file mode 100644 index 0000000..0e62347 --- /dev/null +++ b/patterns/hero/hero-ecommerce-store.php @@ -0,0 +1,47 @@ + __( 'E-commerce Store Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Elevate Your Style This Season

    + + + +

    Discover our curated collection of premium essentials designed for the modern lifestyle. Quality craftsmanship meets contemporary design.

    + + + + +
    + + + +
    +
    Fashion collection showcase
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-education-platform.php b/patterns/hero/hero-education-platform.php new file mode 100644 index 0000000..3006b38 --- /dev/null +++ b/patterns/hero/hero-education-platform.php @@ -0,0 +1,73 @@ + __( 'Education Platform Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Master New Skills with Expert-Led Courses

    + + + +

    Join over 100,000 learners worldwide and transform your career with our comprehensive online courses taught by industry professionals.

    + + + + + + + +
    +
    + + +

    500+ Courses

    +
    + + + +
    + + +

    Expert Instructors

    +
    + + + +
    + + +

    Certificates

    +
    +
    +
    + + + +
    +
    Students learning online
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-event-conference.php b/patterns/hero/hero-event-conference.php new file mode 100644 index 0000000..35f8182 --- /dev/null +++ b/patterns/hero/hero-event-conference.php @@ -0,0 +1,43 @@ + __( 'Event Conference Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Conference stage
    +
    + + +

    TechForward Summit 2026

    + + + +

    The premier conference for innovators, entrepreneurs, and tech leaders shaping the future of technology.

    + + + +
    00
    Days
    00
    Hours
    00
    Min
    00
    Sec
    The countdown has ended!
    + + + + +
    +
    +', +); diff --git a/patterns/hero/hero-event-countdown.php b/patterns/hero/hero-event-countdown.php new file mode 100644 index 0000000..c43806c --- /dev/null +++ b/patterns/hero/hero-event-countdown.php @@ -0,0 +1,67 @@ + __( 'Event Countdown Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    + + +

    The Future of Web Design

    + + + +

    Join us for the biggest product launch of 2025. Reserve your spot today.

    +
    + + + +
    +
    00
    Days
    00
    Hours
    00
    Min
    00
    Sec
    The countdown has ended!
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    + +
    +
    +', +); diff --git a/patterns/hero/hero-fitness-bold.php b/patterns/hero/hero-fitness-bold.php new file mode 100644 index 0000000..48cb3bb --- /dev/null +++ b/patterns/hero/hero-fitness-bold.php @@ -0,0 +1,41 @@ + __( 'Fitness Bold Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Gym interior
    +
    +

    Transform Your Body, Transform Your Life

    + + + +

    Unleash Your
    Full Potential

    + + + +

    State-of-the-art equipment, expert trainers, and a supportive community to help you achieve your fitness goals.

    + + + + +
    +
    +', +); diff --git a/patterns/hero/hero-luxury-realestate.php b/patterns/hero/hero-luxury-realestate.php new file mode 100644 index 0000000..7ae6954 --- /dev/null +++ b/patterns/hero/hero-luxury-realestate.php @@ -0,0 +1,41 @@ + __( 'Luxury Real Estate Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Luxury modern home exterior
    +
    +

    Exceptional Properties

    + + + +

    Discover Your
    Dream Residence

    + + + +

    Curating the world\'s most prestigious properties for discerning buyers who demand excellence.

    + + + + +
    +
    +', +); diff --git a/patterns/hero/hero-modern-saas.php b/patterns/hero/hero-modern-saas.php new file mode 100644 index 0000000..28c154b --- /dev/null +++ b/patterns/hero/hero-modern-saas.php @@ -0,0 +1,41 @@ + __( 'Modern SaaS Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + + +

    Build Better Products
    Ship Faster

    + + + +

    The all-in-one platform that helps teams collaborate, analyze, and deliver exceptional software experiences.

    + + + + + + + +
    Dashboard preview
    +
    +', +); diff --git a/patterns/hero/hero-nonprofit-cause.php b/patterns/hero/hero-nonprofit-cause.php new file mode 100644 index 0000000..7a80f88 --- /dev/null +++ b/patterns/hero/hero-nonprofit-cause.php @@ -0,0 +1,41 @@ + __( 'Nonprofit Cause Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Volunteers helping community
    +
    +

    Making a Difference Since 2010

    + + + +

    Together We Can
    Change Lives

    + + + +

    Join our mission to provide hope, resources, and opportunities to communities in need around the world.

    + + + + +
    +
    +', +); diff --git a/patterns/hero/hero-parallax.php b/patterns/hero/hero-parallax.php new file mode 100644 index 0000000..1f50f72 --- /dev/null +++ b/patterns/hero/hero-parallax.php @@ -0,0 +1,37 @@ + __( 'Hero Parallax', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Modern office workspace
    +
    +

    Transform Your Vision
    Into Reality

    + + + +

    We help businesses grow with innovative solutions and cutting-edge technology.

    + + + + +
    +
    +', +); diff --git a/patterns/hero/hero-portfolio-personal.php b/patterns/hero/hero-portfolio-personal.php new file mode 100644 index 0000000..3408265 --- /dev/null +++ b/patterns/hero/hero-portfolio-personal.php @@ -0,0 +1,69 @@ + __( 'Portfolio Personal Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Hi, I am Sarah

    + + + +

    Product Designer
    & Creative Director

    + + + +

    I help startups and established brands create beautiful, user-centered digital experiences that drive growth and delight users.

    + + + + + + + +
    +

    Connect:

    + + + +
    + + + + + + + + + +
    +
    +
    + + + +
    +
    Sarah - Product Designer
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-professional-services.php b/patterns/hero/hero-professional-services.php new file mode 100644 index 0000000..2ad9934 --- /dev/null +++ b/patterns/hero/hero-professional-services.php @@ -0,0 +1,49 @@ + __( 'Professional Services Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Trusted Advisors Since 1995

    + + + +

    Expert Guidance for Your Most Important Decisions

    + + + +

    We provide strategic counsel to businesses and individuals, helping you navigate complex challenges with confidence and achieve your goals.

    + + + + +
    + + + +
    +
    Professional office
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-restaurant-dining.php b/patterns/hero/hero-restaurant-dining.php new file mode 100644 index 0000000..35932fe --- /dev/null +++ b/patterns/hero/hero-restaurant-dining.php @@ -0,0 +1,79 @@ + __( 'Restaurant Dining Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +
    Chef preparing food
    +
    + + + +
    +

    Our Story

    + + + +

    A Passion for Authentic Flavors

    + + + +

    Founded by Chef Maria Santos, our restaurant celebrates the rich culinary traditions of the Mediterranean with a modern twist.

    + + + +

    Every dish tells a story, crafted with locally-sourced ingredients and time-honored techniques passed down through generations. We believe that great food brings people together, creating moments that last a lifetime.

    + + + +
    + + + +
    +
    +

    15+

    + + + +

    Years of Excellence

    +
    + + + +
    +

    50k+

    + + + +

    Happy Guests

    +
    + + + +
    +

    4.9

    + + + +

    Star Rating

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-saas-startup.php b/patterns/hero/hero-saas-startup.php new file mode 100644 index 0000000..8291326 --- /dev/null +++ b/patterns/hero/hero-saas-startup.php @@ -0,0 +1,43 @@ + __( 'SaaS Startup Hero', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Scale Your Business
    with Powerful Tools

    + + + +

    The all-in-one platform that helps teams collaborate, automate workflows, and deliver results faster than ever before.

    + + + + + + + +

    No credit card required. 14-day free trial.

    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-split.php b/patterns/hero/hero-split.php new file mode 100644 index 0000000..9d48117 --- /dev/null +++ b/patterns/hero/hero-split.php @@ -0,0 +1,43 @@ + __( 'Hero Split', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Transform Your Digital Experience

    + + + +

    Elevate your website with stunning visuals, smooth animations, and powerful customization options that adapt to any design vision.

    + + + + +
    + + + +
    +
    Dashboard preview
    +
    +
    +
    +', +); diff --git a/patterns/hero/hero-video-modal.php b/patterns/hero/hero-video-modal.php new file mode 100644 index 0000000..2fd47c4 --- /dev/null +++ b/patterns/hero/hero-video-modal.php @@ -0,0 +1,43 @@ + __( 'Hero with Video Modal', 'airo-wp' ), + 'categories' => array( 'airo-wp-hero' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    See It In Action

    + + + +

    Watch how our platform transforms the way you build websites

    + + + +
    + + + + + + + +

    No credit card required • Free 14-day trial

    +
    +
    +', +); diff --git a/patterns/homepage/homepage-agency.php b/patterns/homepage/homepage-agency.php new file mode 100644 index 0000000..b5f7668 --- /dev/null +++ b/patterns/homepage/homepage-agency.php @@ -0,0 +1,403 @@ + __( 'Agency / Creative Studio Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Award-Winning Creative Studio

    + + + +

    We Create
    Digital Experiences
    That Matter

    + + + +

    A full-service creative agency specializing in brand strategy, web design, and digital marketing that drives real results.

    + + + + +
    + + + +
    +
    Creative design work
    +
    +
    +
    + + + +
    +

    Trusted by Leading Brands

    + + + +
    +

    VOGUE

    + + + +

    NIKE

    + + + +

    SPOTIFY

    + + + +

    AIRBNB

    +
    +
    + + + +
    +
    +
    +

    What We Do

    + + + +

    Services That Drive Growth

    + + + +

    From strategy to execution, we help ambitious brands stand out in a crowded marketplace.

    +
    + + + +
    +
    +
    0
    Projects Completed
    + + + +
    0
    Industry Awards
    +
    +
    +
    + + + +
    +
    +

    01

    + + + +

    Brand Strategy

    + + + +

    We craft compelling brand identities that resonate with your audience and differentiate you from competitors.

    +
    + + + +
    +

    02

    + + + +

    Web Design

    + + + +

    Beautiful, functional websites that convert visitors into customers and showcase your brand at its best.

    +
    + + + +
    +

    03

    + + + +

    Digital Marketing

    + + + +

    Data-driven campaigns that reach your target audience and deliver measurable business results.

    +
    +
    +
    + + + +
    +
    +
    +
    +

    Selected Work

    + + + +

    Featured Projects

    +
    + + + + +
    +
    + + + +
    +
    +
    Project One
    + + + +

    Luxe Brand Redesign

    + + + +

    Branding / Web Design

    +
    + + + +
    +
    Project Two
    + + + +

    TechStart App Launch

    + + + +

    Digital Marketing / Strategy

    +
    + + + +
    +
    Project Three
    + + + +

    Eco Fashion Campaign

    + + + +

    Social Media / Content

    +
    + + + +
    +
    Project Four
    + + + +

    FinServ Platform

    + + + +

    UX Design / Development

    +
    +
    +
    + + + +
    +
    +

    The Team

    + + + +

    Meet the Creatives

    +
    + + + +
    +
    +
    Alex Rivera
    + + + +

    Alex Rivera

    + + + +

    Creative Director

    +
    + + + +
    +
    Jordan Kim
    + + + +

    Jordan Kim

    + + + +

    Lead Designer

    +
    + + + +
    +
    Marcus Chen
    + + + +

    Marcus Chen

    + + + +

    Strategy Director

    +
    + + + +
    +
    Sophie Taylor
    + + + +

    Sophie Taylor

    + + + +

    Marketing Lead

    +
    +
    +
    + + + +
    +
    +

    Testimonials

    + + + +

    What Clients Say

    +
    + + + +
    +
    +
    +

    "They completely transformed our brand. The results exceeded our expectations, and our customers love the new direction."

    + + + +
    David Park
    + + + +
    +

    Joey Walsh

    + + + +

    CEO, Luxe Brands

    +
    +
    +
    + + + +
    +
    +

    "Working with this team was a game-changer for our startup. They understood our vision and brought it to life beautifully."

    + + + +
    David Park
    + + + +
    +

    David Park

    + + + +

    Founder, TechStart

    +
    +
    +
    +
    +
    + + + +
    +
    +
    +

    Start a Project

    + + + +

    Ready to Bring Your Vision to Life?

    + + + +

    Let us discuss your project and explore how we can help you achieve your goals.

    + + + + +
    + + + +
    +
    +
    + + +

    hello@creativestudio.com

    +
    + + + +
    + + +

    +1 (555) 123-4567

    +
    + + + +
    + + +

    123 Design Street, Creative City

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/homepage/homepage-ecommerce.php b/patterns/homepage/homepage-ecommerce.php new file mode 100644 index 0000000..1a65090 --- /dev/null +++ b/patterns/homepage/homepage-ecommerce.php @@ -0,0 +1,365 @@ + __( 'E-commerce / Online Store Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Elevate Your Style This Season

    + + + +

    Discover our curated collection of premium essentials designed for the modern lifestyle. Quality craftsmanship meets contemporary design.

    + + + + +
    + + + +
    +
    Fashion collection showcase
    +
    +
    +
    + + + +
    +
    +
    0
    Happy Customers
    + + + +
    0
    Products
    + + + +
    0
    Countries
    + + + +
    0
    Rating
    +
    +
    + + + +
    +
    +
    +
    +

    Featured Products

    + + + +

    Best Sellers

    +
    + + + + +
    +
    + + + +
    +
    +
    Premium Sneakers
    + + + +
    +

    Footwear

    + + + +

    Premium Sneakers

    + + + +
    +

    $149

    + + + +

    $99

    +
    +
    +
    + + + +
    +
    Classic Watch
    + + + +
    +

    Accessories

    + + + +

    Classic Watch

    + + + +

    $249

    +
    +
    + + + +
    +
    Leather Backpack
    + + + +
    +

    Bags

    + + + +

    Leather Backpack

    + + + +

    $189

    +
    +
    + + + +
    +
    Designer Sunglasses
    + + + +
    +

    Eyewear

    + + + +

    Designer Sunglasses

    + + + +

    $129

    +
    +
    +
    +
    + + + +
    +
    +
    + + +

    Free Shipping

    + + + +

    Free shipping on all orders over $50. Fast delivery within 3-5 business days.

    +
    + + + +
    + + +

    Easy Returns

    + + + +

    30-day hassle-free returns. Not satisfied? Get a full refund, no questions asked.

    +
    + + + +
    + + +

    Secure Payment

    + + + +

    Your payment information is protected with industry-leading encryption.

    +
    +
    +
    + + + +
    +
    +

    Customer Love

    + + + +

    What Our Customers Say

    +
    + + + +
    +
    +
    +

    ★★★★★

    + + + +

    "Absolutely love the quality! The sneakers are so comfortable and stylish. Already planning my next purchase."

    + + + +

    — Emma S.

    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "Fast shipping and excellent customer service. The watch exceeded my expectations. Highly recommend!"

    + + + +

    — Michael R.

    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "The leather backpack is gorgeous and so well-made. Perfect for work and weekend trips alike."

    + + + +

    — Sarah L.

    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "Best online shopping experience I have had. The quality is unmatched and the prices are fair."

    + + + +

    — David K.

    +
    +
    +
    +
    + + + +
    +
    +
    +

    FAQ

    + + + +

    Frequently Asked Questions

    + + + +

    Find answers to common questions about our products, shipping, and return policies.

    + + + +
    +
    +

    Standard shipping takes 3-5 business days within the US. Express shipping (1-2 days) is available for an additional fee. International orders typically arrive within 7-14 business days.

    +
    + + + +
    + + + +
    + + + +
    +
    +
    + + + +
    +
    Shopping experience
    +
    +
    +
    + + + +
    +
    +

    Get 15% Off Your First Order

    + + + +

    Subscribe to our newsletter for exclusive deals, new arrivals, and style inspiration delivered straight to your inbox.

    + + + + + + + +

    No spam, unsubscribe anytime. By subscribing you agree to our Privacy Policy.

    +
    +
    +', +); diff --git a/patterns/homepage/homepage-education.php b/patterns/homepage/homepage-education.php new file mode 100644 index 0000000..6664622 --- /dev/null +++ b/patterns/homepage/homepage-education.php @@ -0,0 +1,473 @@ + __( 'Education / Online Learning Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + + +

    Master New Skills with Expert-Led Courses

    + + + +

    Join over 100,000 learners worldwide and transform your career with our comprehensive online courses taught by industry professionals.

    + + + + + + + +
    +
    + + +

    500+ Courses

    +
    + + + +
    + + +

    Expert Instructors

    +
    + + + +
    + + +

    Certificates

    +
    +
    +
    + + + +
    +
    Students learning online
    +
    +
    +
    + + + +
    +
    +
    0
    Students Enrolled
    + + + +
    0
    Expert Courses
    + + + +
    0
    Certified Instructors
    + + + +
    0
    Satisfaction Rate
    +
    +
    + + + +
    +
    +

    Popular Categories

    + + + +

    Explore Learning Paths

    + + + +

    Choose from a wide range of categories to find the perfect course for your career goals.

    +
    + + + +
    +
    + + +

    Web Development

    + + + +

    120+ Courses

    +
    + + + +
    + + +

    Data Science

    + + + +

    85+ Courses

    +
    + + + +
    + + +

    Design & UX

    + + + +

    95+ Courses

    +
    + + + +
    + + +

    Business & Marketing

    + + + +

    200+ Courses

    +
    +
    +
    + + + +
    +
    +
    +
    +

    Featured Courses

    + + + +

    Top Rated This Month

    +
    + + + + +
    +
    + + + +
    +
    +
    Web development course
    + + + +
    +
    + + +

    ★★★★★ 4.9

    +
    + + + +

    Complete Web Development Bootcamp

    + + + +

    Dr. Angela Yu • 65 hours

    + + + +
    +

    $89.99

    + + + +

    $199.99

    +
    +
    +
    + + + +
    +
    Data science course
    + + + +
    +
    + + +

    ★★★★★ 4.8

    +
    + + + +

    Python for Data Science & Machine Learning

    + + + +

    Jose Portilla • 45 hours

    + + + +
    +

    $94.99

    + + + +

    $179.99

    +
    +
    +
    + + + +
    +
    UI/UX design course
    + + + +
    +
    + + +

    ★★★★★ 4.9

    +
    + + + +

    UI/UX Design Masterclass

    + + + +

    Sarah Chen • 38 hours

    + + + +
    +

    $79.99

    + + + +

    $149.99

    +
    +
    +
    +
    +
    + + + +
    +
    +

    Expert Instructors

    + + + +

    Learn from Industry Leaders

    +
    + + + +
    +
    +
    Dr. Angela Yu
    + + + +

    Dr. Angela Yu

    + + + +

    Web Development

    +
    + + + +
    +
    Jose Portilla
    + + + +

    Jose Portilla

    + + + +

    Data Science

    +
    + + + +
    +
    Sarah Chen
    + + + +

    Sarah Chen

    + + + +

    UI/UX Design

    +
    + + + +
    +
    Michael Roberts
    + + + +

    Michael Roberts

    + + + +

    Marketing

    +
    +
    +
    + + + +
    +
    +

    Student Success

    + + + +

    What Our Students Say

    +
    + + + +
    +
    +
    +

    ★★★★★

    + + + +

    "This platform completely changed my career. I went from knowing nothing about coding to landing my first developer job in just 6 months. The instructors are amazing!"

    + + + +
    +
    Alex
    + + + +
    +

    Alex Thompson

    + + + +

    Frontend Developer at Google

    +
    +
    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "The data science course was incredibly comprehensive. I learned Python, machine learning, and got hands-on experience with real projects. Worth every penny!"

    + + + +
    +
    Emily
    + + + +
    +

    Emily Rodriguez

    + + + +

    Data Analyst at Amazon

    +
    +
    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "As someone who switched careers at 35, these courses gave me the confidence and skills I needed. Now I am a senior UX designer. It is never too late to learn!"

    + + + +
    +
    Marcus
    + + + +
    +

    Marcus Johnson

    + + + +

    Senior UX Designer at Microsoft

    +
    +
    +
    +
    +
    +
    + + + +
    +
    +

    Start Your Learning Journey Today

    + + + +

    Join our community of learners and unlock your potential. Get unlimited access to all courses with our subscription plans.

    + + + + +
    +
    +', +); diff --git a/patterns/homepage/homepage-event.php b/patterns/homepage/homepage-event.php new file mode 100644 index 0000000..218f518 --- /dev/null +++ b/patterns/homepage/homepage-event.php @@ -0,0 +1,545 @@ + __( 'Event / Conference Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Conference stage
    +
    + + +

    TechForward Summit 2026

    + + + +

    The premier conference for innovators, entrepreneurs, and tech leaders shaping the future of technology.

    + + + +
    00
    Days
    00
    Hours
    00
    Min
    00
    Sec
    The countdown has ended!
    + + + + +
    +
    + + + +
    +
    +
    0
    World-Class Speakers
    + + + +
    0
    Days of Innovation
    + + + +
    0
    Sessions & Workshops
    + + + +
    0
    Expected Attendees
    +
    +
    + + + +
    +
    +

    Featured Speakers

    + + + +

    Learn from Industry Leaders

    + + + +

    Hear from visionaries who are transforming industries and shaping the future of technology.

    +
    + + + +
    +
    +
    Speaker James Chen
    + + + +
    +

    James Chen

    + + + +

    CEO, TechVenture Labs

    + + + +

    AI & Machine Learning

    +
    +
    + + + +
    +
    Speaker Sarah Miller
    + + + +
    +

    Sarah Miller

    + + + +

    VP Engineering, CloudScale

    + + + +

    Cloud Architecture

    +
    +
    + + + +
    +
    Speaker Michael Roberts
    + + + +
    +

    Michael Roberts

    + + + +

    Founder, DataDriven

    + + + +

    Data Science

    +
    +
    + + + +
    +
    Speaker Emily Zhang
    + + + +
    +

    Emily Zhang

    + + + +

    CTO, SecureNet

    + + + +

    Cybersecurity

    +
    +
    +
    + + + + +
    + + + +
    +
    +

    Event Schedule

    + + + +

    Three Days of Innovation

    +
    + + + +
    + + + + + + + + + +
    +
    + + + +
    +
    +

    Tickets

    + + + +

    Secure Your Spot

    + + + +

    Early bird pricing available for a limited time. Choose the pass that fits your needs.

    +
    + + + +
    +
    +

    General Pass

    + + + +
    +

    $

    + + + +

    499

    +
    + + + +

    Early Bird Price

    + + + +
    + + + +
    +

    3-day conference access

    + + + +

    All keynotes & sessions

    + + + +

    Networking events

    + + + +

    Conference materials

    +
    + + + + +
    + + + +
    + + +

    VIP Pass

    + + + +
    +

    $

    + + + +

    899

    +
    + + + +

    Early Bird Price

    + + + +
    + + + +
    +

    Everything in General

    + + + +

    VIP lounge access

    + + + +

    Speaker meet & greet

    + + + +

    VIP dinner & awards

    + + + +

    Priority seating

    +
    + + + + +
    + + + +
    +

    Corporate Pass

    + + + +
    +

    $

    + + + +

    3,999

    +
    + + + +

    5 Team Members

    + + + +
    + + + +
    +

    5 VIP passes

    + + + +

    Dedicated account manager

    + + + +

    Private meeting room

    + + + +

    Logo on event materials

    +
    + + + + +
    +
    +
    + + + +
    +

    Our Sponsors

    + + + +
    +

    TECHCORP

    + + + +

    CLOUDIFY

    + + + +

    DATAFLOW

    + + + +

    NEXUSAI

    + + + +

    SCALABLE

    +
    +
    + + + +
    +
    +

    Ready to Join the Future?

    + + + +

    Do not miss the most anticipated tech event of 2026. Limited seats available - secure yours today!

    + + + + +
    +
    +', +); diff --git a/patterns/homepage/homepage-fitness.php b/patterns/homepage/homepage-fitness.php new file mode 100644 index 0000000..35fa772 --- /dev/null +++ b/patterns/homepage/homepage-fitness.php @@ -0,0 +1,473 @@ + __( 'Fitness / Gym Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Gym interior
    +
    +

    Transform Your Body, Transform Your Life

    + + + +

    Unleash Your
    Full Potential

    + + + +

    State-of-the-art equipment, expert trainers, and a supportive community to help you achieve your fitness goals.

    + + + + +
    +
    + + + +
    +
    +
    0
    Active Members
    + + + +
    0
    Expert Trainers
    + + + +
    0
    Weekly Classes
    + + + +
    0
    Years Experience
    +
    +
    + + + +
    +
    +

    Our Classes

    + + + +

    Train Your Way

    + + + +

    From high-intensity workouts to mindful yoga, we offer classes for every fitness level and goal.

    +
    + + + +
    +
    + + +

    HIIT Training

    + + + +

    High-intensity interval training to maximize calorie burn and boost metabolism.

    + + + +

    45 min • All Levels

    +
    + + + +
    + + +

    Strength Training

    + + + +

    Build muscle and increase strength with our expert-led weight training sessions.

    + + + +

    60 min • Intermediate

    +
    + + + +
    + + +

    Yoga & Pilates

    + + + +

    Improve flexibility, balance, and mental clarity with mindful movement practices.

    + + + +

    60 min • All Levels

    +
    + + + +
    + + +

    Group Cycling

    + + + +

    High-energy indoor cycling with motivating music and inspiring instructors.

    + + + +

    45 min • All Levels

    +
    +
    + + + + +
    + + + +
    +
    +
    +
    Personal training session
    +
    + + + +
    +

    Personal Training

    + + + +

    One-on-One Coaching

    + + + +

    Get personalized attention and a customized workout plan designed specifically for your goals, fitness level, and schedule.

    + + + +
    +
    + + +

    Custom workout plans

    +
    + + + +
    + + +

    Nutrition guidance

    +
    + + + +
    + + +

    Progress tracking

    +
    + + + +
    + + +

    Flexible scheduling

    +
    +
    + + + + +
    +
    +
    + + + +
    +
    +

    Membership Plans

    + + + +

    Choose Your Plan

    +
    + + + +
    +
    +

    Basic

    + + + +

    Perfect for getting started

    + + + +
    +

    $

    + + + +

    29

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Gym access (6am-10pm)

    + + + +

    Basic equipment

    + + + +

    Locker room access

    + + + +

    Free WiFi

    +
    + + + + +
    + + + +
    + + +

    Premium

    + + + +

    Everything you need to succeed

    + + + +
    +

    $

    + + + +

    59

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    24/7 gym access

    + + + +

    All equipment & classes

    + + + +

    1 PT session/month

    + + + +

    Sauna & spa access

    + + + +

    Guest passes (2/month)

    +
    + + + + +
    + + + +
    +

    Elite

    + + + +

    The ultimate fitness experience

    + + + +
    +

    $

    + + + +

    99

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Everything in Premium

    + + + +

    4 PT sessions/month

    + + + +

    Nutrition coaching

    + + + +

    Recovery treatments

    + + + +

    Priority booking

    +
    + + + + +
    +
    +
    + + + +
    +
    +

    Expert Trainers

    + + + +

    Meet Our Team

    +
    + + + +
    +
    +
    Trainer Alex
    + + + +

    Alex Rodriguez

    + + + +

    Head Trainer

    +
    + + + +
    +
    Trainer Maya
    + + + +

    Maya Johnson

    + + + +

    Yoga & Pilates

    +
    + + + +
    +
    Trainer Chris
    + + + +

    Chris Martinez

    + + + +

    Strength Coach

    +
    + + + +
    +
    Trainer Sarah
    + + + +

    Sarah Kim

    + + + +

    Cycling & HIIT

    +
    +
    +
    + + + +
    +
    +

    Ready to Start Your Transformation?

    + + + +

    Join today and get your first week free. No commitment, no risk. Just results.

    + + + + +
    +
    +', +); diff --git a/patterns/homepage/homepage-luxury-realestate.php b/patterns/homepage/homepage-luxury-realestate.php new file mode 100644 index 0000000..283e601 --- /dev/null +++ b/patterns/homepage/homepage-luxury-realestate.php @@ -0,0 +1,407 @@ + __( 'Luxury Real Estate Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Luxury modern home exterior
    +
    +

    Exceptional Properties

    + + + +

    Discover Your
    Dream Residence

    + + + +

    Curating the world\'s most prestigious properties for discerning buyers who demand excellence.

    + + + + +
    +
    + + + +
    +
    +
    +

    About Us

    + + + +

    A Legacy of Excellence in Luxury Real Estate

    + + + +

    For over two decades, we have been the trusted advisors for clients seeking extraordinary properties. Our deep expertise in luxury markets, combined with unparalleled discretion and personalized service, sets us apart.

    + + + +

    We understand that finding the perfect home is about more than square footage and amenities. It is about discovering a residence that reflects your lifestyle, aspirations, and vision for the future.

    + + + +
    +
    0
    In Sales Volume
    + + + +
    0
    Properties Sold
    +
    +
    + + + +
    +
    Luxury home interior
    +
    +
    +
    + + + +
    +
    +

    Featured Listings

    + + + +

    Exceptional Properties Await

    +
    + + + +
    +
    +

    Beverly Hills

    + + + +

    Modern Estate

    + + + +

    $12,500,000 | 6 Bed | 8 Bath

    +
    + + + +
    +

    Malibu

    + + + +

    Oceanfront Villa

    + + + +

    $28,000,000 | 5 Bed | 7 Bath

    +
    + + + +
    +

    Bel Air

    + + + +

    Contemporary Mansion

    + + + +

    $45,000,000 | 8 Bed | 12 Bath

    +
    + + + +
    +

    Hollywood Hills

    + + + +

    Architectural Masterpiece

    + + + +

    $18,900,000 | 5 Bed | 6 Bath

    +
    +
    + + + + +
    + + + +
    +
    +

    Our Services

    + + + +

    White-Glove Service at Every Step

    +
    + + + +
    +
    +
    + + +

    Buyer Representation

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Buyer Representation

    + + +

    Our expert agents guide you through every step of finding and acquiring your perfect property. From initial search to closing, we negotiate on your behalf to secure the best terms.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Property Valuation

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Property Valuation

    + + +

    Receive accurate, data-driven valuations backed by our deep knowledge of luxury markets. We analyze comparable sales, market trends, and unique property features.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Global Reach

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Global Reach

    + + +

    Access exclusive listings worldwide through our international network. From New York penthouses to Mediterranean villas, we connect you with extraordinary properties globally.

    + + +

    Learn more →

    + +
    +
    +
    +
    + + + +
    +
    +

    Testimonials

    + + + +

    What Our Clients Say

    +
    + + + +
    +
    +
    +

    "The team made finding our dream home an absolute pleasure. Their knowledge of the luxury market is unmatched, and they found us a property that exceeded all our expectations."

    + + + +
    +
    Client photo
    + + + +
    +

    Michael Richardson

    + + + +

    Beverly Hills Buyer

    +
    +
    +
    +
    + + + +
    +
    +

    "Selling our estate was a seamless experience. They positioned our property perfectly in the market and attracted qualified buyers from around the world. Truly exceptional service."

    + + + +
    +
    Client photo
    + + + +
    +

    Sarah Chen

    + + + +

    Bel Air Seller

    +
    +
    +
    +
    + + + +
    +
    +

    "As international buyers, we relied heavily on their expertise. They navigated complex legal requirements and found us a stunning oceanfront property that serves as our perfect retreat."

    + + + +
    +
    Client photo
    + + + +
    +

    David Laurent

    + + + +

    International Buyer

    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +

    Contact Us

    + + + +

    Begin Your Journey to Extraordinary Living

    + + + +

    Whether you are searching for your dream home or considering selling your property, our team is ready to provide the exceptional service you deserve.

    + + + +
    +
    + + +

    +1 (310) 555-0100

    +
    + + + +
    + + +

    inquiries@luxuryestates.com

    +
    + + + +
    + + +

    9876 Wilshire Blvd, Beverly Hills, CA 90210

    +
    +
    +
    + + + +
    +

    Schedule a Consultation

    + + + +
    +
    + + + + + + + +
    + + + +
    +
    +
    +
    +
    +', +); diff --git a/patterns/homepage/homepage-modern-saas.php b/patterns/homepage/homepage-modern-saas.php new file mode 100644 index 0000000..1217d37 --- /dev/null +++ b/patterns/homepage/homepage-modern-saas.php @@ -0,0 +1,441 @@ + __( 'Modern SaaS Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + + +

    Build Better Products
    Ship Faster

    + + + +

    The all-in-one platform that helps teams collaborate, analyze, and deliver exceptional software experiences.

    + + + + + + + +
    Dashboard preview
    +
    + + + +
    +

    TRUSTED BY INNOVATIVE TEAMS WORLDWIDE

    + + + +
    +

    STRIPE

    + + + +

    NOTION

    + + + +

    FIGMA

    + + + +

    VERCEL

    + + + +

    LINEAR

    +
    +
    + + + +
    +
    +

    Features

    + + + +

    Everything You Need to Scale

    + + + +

    Powerful tools designed to help your team move faster and build better products.

    +
    + + + +
    +
    +
    + + +

    Real-time Analytics

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Real-time Analytics

    + + +

    Track every metric that matters with live dashboards. Get instant insights into user behavior, conversion rates, and performance metrics.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Team Collaboration

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Team Collaboration

    + + +

    Work together seamlessly with real-time editing, comments, and shared workspaces. Keep everyone aligned and moving fast.

    + + +

    Learn more →

    + +
    +
    + + + +
    +
    + + +

    Automation

    + + +

    Hover to learn more

    + +
    + + + +
    + +

    Automation

    + + +

    Automate repetitive tasks and workflows. Set up triggers, actions, and rules that work while you sleep.

    + + +

    Learn more →

    + +
    +
    +
    +
    + + + +
    +
    +
    +

    Why Choose Us

    + + + +

    Built for Teams Who Ship

    + + + +

    We understand the challenges of modern product development. That is why we built a platform that gets out of your way and lets you focus on what matters.

    + + + +
    +
    0
    Uptime SLA
    + + + +
    0
    Avg Response
    + + + +
    0
    Active Users
    + + + +
    0
    Support
    +
    +
    + + + +
    +
    Analytics dashboard
    +
    +
    +
    + + + +
    +
    +

    Product Showcase

    + + + +

    See It in Action

    +
    + + + +
    +
    +

    Dashboard

    + + + +

    Powerful analytics at your fingertips

    +
    + + + +
    +

    Reports

    + + + +

    Generate insights in seconds

    +
    + + + +
    +

    Team Hub

    + + + +

    Collaborate in real-time

    +
    + + + +
    +

    Integrations

    + + + +

    Connect with 100+ tools

    +
    +
    +
    + + + +
    +
    +

    Pricing

    + + + +

    Simple, Transparent Pricing

    +
    + + + +
    +
    +

    Starter

    + + + +
    +

    $

    + + + +

    0

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Up to 3 team members

    + + + +

    Basic analytics

    + + + +

    1GB storage

    + + + +

    Community support

    +
    + + + + +
    + + + +
    + + +

    Pro

    + + + +
    +

    $

    + + + +

    29

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Unlimited team members

    + + + +

    Advanced analytics

    + + + +

    100GB storage

    + + + +

    Priority support

    + + + +

    API access

    +
    + + + + +
    + + + +
    +

    Enterprise

    + + + +
    +

    Custom

    +
    + + + +
    + + + +
    +

    Everything in Pro

    + + + +

    Custom integrations

    + + + +

    Unlimited storage

    + + + +

    Dedicated support

    + + + +

    SLA guarantee

    +
    + + + + +
    +
    +
    + + + +
    +

    Ready to Transform Your Workflow?

    + + + +

    Join thousands of teams already using our platform to ship better products faster. Start your free trial today.

    + + + + +
    +', +); diff --git a/patterns/homepage/homepage-nonprofit.php b/patterns/homepage/homepage-nonprofit.php new file mode 100644 index 0000000..b5e5174 --- /dev/null +++ b/patterns/homepage/homepage-nonprofit.php @@ -0,0 +1,351 @@ + __( 'Non-profit / Charity Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Volunteers helping community
    +
    +

    Making a Difference Since 2010

    + + + +

    Together We Can
    Change Lives

    + + + +

    Join our mission to provide hope, resources, and opportunities to communities in need around the world.

    + + + + +
    +
    + + + +
    +
    +
    0
    Lives Changed
    + + + +
    0
    Countries Reached
    + + + +
    0
    Volunteers
    + + + +
    0
    Raised
    +
    +
    + + + +
    +
    +
    +

    Our Mission

    + + + +

    Empowering Communities, One Life at a Time

    + + + +

    We believe everyone deserves access to clean water, education, healthcare, and economic opportunity. Through sustainable programs and community partnerships, we work to break the cycle of poverty.

    + + + +

    Our approach focuses on long-term solutions that empower communities to thrive independently. We invest in local leaders, provide training and resources, and measure our impact to ensure every dollar makes a difference.

    + + + + +
    + + + +
    +
    Children receiving education
    +
    +
    +
    + + + +
    +
    +

    Our Focus Areas

    + + + +

    Where Your Support Goes

    +
    + + + +
    +
    + + +

    Education

    + + + +

    Building schools and providing scholarships to give children the education they deserve.

    +
    + + + +
    + + +

    Clean Water

    + + + +

    Installing wells and water systems to provide safe drinking water to communities.

    +
    + + + +
    + + +

    Healthcare

    + + + +

    Funding medical clinics and providing essential healthcare services to underserved areas.

    +
    + + + +
    + + +

    Economic Empowerment

    + + + +

    Microloans and vocational training to help families build sustainable livelihoods.

    +
    +
    +
    + + + +
    +
    +

    Current Campaign

    + + + +

    Help Us Reach Our Goal

    + + + +

    We are building 10 new schools in rural communities. Every donation brings us closer to making education accessible to 5,000 more children.

    +
    + + + +
    +
    75%
    + + + + +
    +
    + + + +
    +
    +

    Success Stories

    + + + +

    Real Impact, Real Stories

    +
    + + + +
    +
    +
    +
    Maria with her family
    + + + +

    Maria\'s Journey to Self-Sufficiency

    + + + +

    "Thanks to the microloan program, I was able to start my own tailoring business. Now I can provide for my children and send them to school. This organization gave me hope when I had none."

    + + + +

    — Maria, Guatemala

    +
    +
    + + + +
    +
    +
    Village with clean water
    + + + +

    Clean Water for Kibera Village

    + + + +

    "Before the well was built, we walked 3 miles every day for water. Now our children are healthier and can focus on their education instead of carrying water."

    + + + +

    — Village Elder, Kenya

    +
    +
    + + + +
    +
    +
    Students in classroom
    + + + +

    First in Her Family to Graduate

    + + + +

    "The scholarship changed everything. I became the first person in my family to finish high school. Now I am studying to become a doctor so I can help my community."

    + + + +

    — Amara, Nigeria

    +
    +
    +
    +
    + + + +
    +
    +
    +

    Get Involved

    + + + +

    Ways to Make a Difference

    + + + +

    There are many ways to support our mission beyond financial donations. Your time, skills, and voice can make a tremendous impact.

    + + + +
    +
    +

    Join our volunteer network and make a hands-on difference. We offer both local and international volunteer opportunities, from community events to field missions.

    +
    + + + +
    + + + +
    + + + +
    +
    +
    + + + +
    +

    Sign Up to Volunteer

    + + + +
    +
    + + + + + + + +
    +
    +
    +
    +
    + + + +
    +
    +

    Every Gift Creates Ripples of Change

    + + + +

    Your donation today will transform lives for generations to come. Together, we can build a world where everyone has the opportunity to thrive.

    + + + + +
    +
    +', +); diff --git a/patterns/homepage/homepage-portfolio.php b/patterns/homepage/homepage-portfolio.php new file mode 100644 index 0000000..531f5a8 --- /dev/null +++ b/patterns/homepage/homepage-portfolio.php @@ -0,0 +1,373 @@ + __( 'Portfolio / Personal Brand Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Hi, I am Sarah

    + + + +

    Product Designer
    & Creative Director

    + + + +

    I help startups and established brands create beautiful, user-centered digital experiences that drive growth and delight users.

    + + + + + + + +
    +

    Connect:

    + + + +
    + + + + + + + + + +
    +
    +
    + + + +
    +
    Sarah - Product Designer
    +
    +
    +
    + + + +
    +

    Previously worked with

    + + + +
    +

    Google

    + + + +

    Spotify

    + + + +

    Airbnb

    + + + +

    Stripe

    + + + +

    Notion

    +
    +
    + + + +
    +
    +
    +

    About Me

    + + + +

    Designing with Purpose, Creating with Passion

    + + + +

    With over 10 years of experience in product design, I have helped companies ranging from early-stage startups to Fortune 500 companies create meaningful digital experiences.

    + + + +

    I believe great design is invisible - it should feel intuitive and delightful without getting in the way. My approach combines user research, strategic thinking, and pixel-perfect execution to deliver results that matter.

    + + + +

    When not designing, you will find me mentoring aspiring designers, speaking at conferences, or exploring hiking trails with my camera.

    +
    + + + +
    +
    +
    0
    Years Experience
    + + + +
    0
    Projects Delivered
    + + + +
    0
    Happy Clients
    + + + +
    0
    Design Awards
    +
    +
    +
    +
    + + + +
    +
    +

    What I Offer

    + + + +

    Services

    +
    + + + +
    +
    + + +

    Product Design

    + + + +

    End-to-end product design from concept to launch, including user research, wireframing, and high-fidelity prototypes.

    +
    + + + +
    + + +

    UI/UX Design

    + + + +

    Intuitive interfaces and seamless experiences that delight users and achieve business goals.

    +
    + + + +
    + + +

    Brand Identity

    + + + +

    Visual identity systems including logos, color palettes, typography, and brand guidelines.

    +
    +
    +
    + + + +
    +
    +
    +
    +

    Portfolio

    + + + +

    Selected Work

    +
    + + + + +
    +
    + + + +
    +
    +
    Mobile App Design
    + + + +

    FinTech Mobile App

    + + + +

    Product Design / Mobile App

    +
    + + + +
    +
    Dashboard Design
    + + + +

    SaaS Dashboard Redesign

    + + + +

    UX Design / Web App

    +
    + + + +
    +
    E-commerce Design
    + + + +

    E-commerce Experience

    + + + +

    UI Design / E-commerce

    +
    + + + +
    +
    Brand Identity
    + + + +

    Startup Brand Identity

    + + + +

    Branding / Visual Identity

    +
    +
    +
    + + + +
    +
    +

    Kind Words

    + + + +

    What Clients Say

    +
    + + + +
    +
    +

    "Sarah has an incredible ability to understand complex problems and translate them into elegant, user-friendly solutions. She transformed our product completely."

    + + + +
    +
    Jason Miller
    + + + +
    +

    Jason Miller

    + + + +

    CEO, TechVenture

    +
    +
    +
    + + + +
    +

    "Working with Sarah was a dream. She is not just a designer but a true partner who cares deeply about the success of your product. Highly recommended!"

    + + + +
    +
    Lisa Chen
    + + + +
    +

    Lisa Chen

    + + + +

    Product Lead, InnovateCo

    +
    +
    +
    +
    +
    + + + +
    +
    +

    Get in Touch

    + + + +

    Let us Work Together

    + + + +

    Have a project in mind? I would love to hear about it. Drop me a message and let us create something amazing together.

    + + + + + + + +
    + + + + + + + + + + + + + +
    +
    +
    +', +); diff --git a/patterns/homepage/homepage-professional.php b/patterns/homepage/homepage-professional.php new file mode 100644 index 0000000..f80486f --- /dev/null +++ b/patterns/homepage/homepage-professional.php @@ -0,0 +1,491 @@ + __( 'Professional Services Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Trusted Advisors Since 1995

    + + + +

    Expert Guidance for Your Most Important Decisions

    + + + +

    We provide strategic counsel to businesses and individuals, helping you navigate complex challenges with confidence and achieve your goals.

    + + + + +
    + + + +
    +
    Professional office
    +
    +
    +
    + + + +
    + +
    + + + +
    +
    +

    Our Practice Areas

    + + + +

    Comprehensive expertise across key areas to serve your needs.

    +
    + + + +
    +
    + + +

    Business Law

    + + + +

    Corporate formation, contracts, mergers and acquisitions, and business dispute resolution.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Real Estate

    + + + +

    Commercial and residential transactions, zoning, development, and property disputes.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Litigation

    + + + +

    Civil litigation, dispute resolution, arbitration, and aggressive courtroom representation.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Employment Law

    + + + +

    Workplace compliance, discrimination claims, employment contracts, and HR consulting.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Estate Planning

    + + + +

    Wills, trusts, estate administration, and wealth transfer strategies.

    + + + +

    Learn more

    +
    + + + +
    + + +

    Tax Advisory

    + + + +

    Tax planning, IRS representation, and strategic tax minimization for individuals and businesses.

    + + + +

    Learn more

    +
    +
    +
    + + + +
    +
    +
    +

    Why Clients Choose Us

    + + + +

    We combine deep expertise with personalized service to deliver exceptional outcomes for every client.

    + + + +
    +
    + + +
    +

    Proven Track Record

    + + + +

    Over 30 years of successful outcomes for clients across industries.

    +
    +
    + + + +
    + + +
    +

    Personalized Attention

    + + + +

    Direct access to senior partners who personally handle your matters.

    +
    +
    + + + +
    + + +
    +

    Transparent Communication

    + + + +

    Clear updates and honest advice throughout your engagement.

    +
    +
    + + + +
    + + +
    +

    Results-Driven Approach

    + + + +

    Focused strategies designed to achieve your specific objectives.

    +
    +
    +
    +
    + + + +
    +
    Business handshake
    +
    +
    +
    + + + +
    +
    +

    Our Leadership Team

    + + + +

    Experienced professionals dedicated to your success.

    +
    + + + +
    +
    +
    Robert Mitchell
    + + + +

    Robert Mitchell

    + + + +

    Managing Partner

    + + + +

    Harvard Law, 25+ years in corporate law and complex litigation.

    +
    + + + +
    +
    Sarah Williams
    + + + +

    Sarah Williams

    + + + +

    Senior Partner, Real Estate

    + + + +

    Yale Law, specializing in commercial real estate and development.

    +
    + + + +
    +
    Michael Chen
    + + + +

    Michael Chen

    + + + +

    Senior Partner, Tax Advisory

    + + + +

    Stanford Law, CPA with expertise in tax strategy and IRS matters.

    +
    +
    +
    + + + +
    +
    +

    What Our Clients Say

    +
    + + + +
    +
    +

    "They handled our company merger with exceptional skill and attention to detail. Their guidance was invaluable throughout the entire process."

    + + + +
    +
    Thomas Anderson
    + + + +
    +

    Thomas Anderson

    + + + +

    CEO, Anderson Industries

    +
    +
    +
    + + + +
    +

    "Professional, responsive, and truly invested in our success. They have been our trusted advisors for over a decade now."

    + + + +
    +
    Patricia Reynolds
    + + + +
    +

    Patricia Reynolds

    + + + +

    Managing Director, Reynolds Group

    +
    +
    +
    +
    +
    + + + +
    +
    +

    Frequently Asked Questions

    +
    + + + +
    +
    +

    Please bring any relevant documents related to your matter, such as contracts, correspondence, or financial statements. A written summary of your situation and questions you would like answered is also helpful.

    +
    + + + +
    + + + +
    + + + +
    +
    +
    + + + +
    +
    +
    +

    Schedule Your Free Consultation

    + + + +

    Take the first step toward resolving your legal matter. Contact us today to schedule a confidential consultation with one of our experienced attorneys.

    + + + +
    +
    + + +

    123 Legal Plaza, Suite 500, Business City, ST 12345

    +
    + + + +
    + + +

    (555) 123-4567

    +
    + + + +
    + + +

    contact@professionalfirm.com

    +
    + + + +
    + + +

    Monday - Friday: 8:30 AM - 6:00 PM

    +
    +
    +
    + + + +
    +
    +

    Request a Consultation

    + + + +
    +
    + + + + + + + +
    + + + +
    + + + +
    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/homepage/homepage-restaurant.php b/patterns/homepage/homepage-restaurant.php new file mode 100644 index 0000000..de00f70 --- /dev/null +++ b/patterns/homepage/homepage-restaurant.php @@ -0,0 +1,361 @@ + __( 'Restaurant / Local Business Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    Restaurant interior
    +
    +

    Est. 2010 - Farm to Table Dining

    + + + +

    Where Every Meal
    Becomes a Memory

    + + + +

    Experience the finest seasonal ingredients crafted with passion and served with warmth in the heart of downtown.

    + + + + +
    +
    + + + +
    +
    +
    +
    Chef preparing food
    +
    + + + +
    +

    Our Story

    + + + +

    A Passion for Authentic Flavors

    + + + +

    Founded by Chef Maria Santos, our restaurant celebrates the rich culinary traditions of the Mediterranean with a modern twist.

    + + + +

    Every dish tells a story, crafted with locally-sourced ingredients and time-honored techniques passed down through generations. We believe that great food brings people together, creating moments that last a lifetime.

    + + + +
    + + + +
    +
    +

    15+

    + + + +

    Years of Excellence

    +
    + + + +
    +

    50k+

    + + + +

    Happy Guests

    +
    + + + +
    +

    4.9

    + + + +

    Star Rating

    +
    +
    +
    +
    +
    + + + +
    +
    +

    Chef Favorites

    + + + +

    Signature Dishes

    + + + +

    Our most beloved creations, made with the freshest seasonal ingredients.

    +
    + + + +
    +
    +
    Grilled Salmon
    + + + +
    +

    Herb-Crusted Salmon

    + + + +

    $34

    +
    + + + +

    Wild-caught salmon with lemon herb crust, served with seasonal vegetables and saffron risotto.

    +
    + + + +
    +
    Beef Tenderloin
    + + + +
    +

    Prime Beef Tenderloin

    + + + +

    $48

    +
    + + + +

    Aged tenderloin with red wine reduction, truffle mashed potatoes, and grilled asparagus.

    +
    + + + +
    +
    Lobster Risotto
    + + + +
    +

    Lobster Risotto

    + + + +

    $42

    +
    + + + +

    Maine lobster tail atop creamy Arborio rice with saffron, white wine, and fresh herbs.

    +
    +
    + + + + +
    + + + +
    +
    +

    The Experience

    + + + +

    Our Atmosphere

    +
    + + + +
    +
    Restaurant interior 1
    + + + +
    Restaurant interior 2
    + + + +
    Restaurant interior 3
    + + + +
    Restaurant interior 4
    +
    +
    + + + +
    +
    +

    Reviews

    + + + +

    What Our Guests Say

    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "An unforgettable dining experience! The lobster risotto was pure perfection. Will definitely be back."

    + + + +

    - Michael R., via Google

    +
    + + + +
    +

    ★★★★★

    + + + +

    "We celebrated our anniversary here and it was magical. The service was impeccable and the food was outstanding."

    + + + +

    - Sarah T., via Yelp

    +
    + + + +
    +

    ★★★★★

    + + + +

    "Best restaurant in the city, hands down. The chef truly understands how to bring out the best in each ingredient."

    + + + +

    - James L., via TripAdvisor

    +
    +
    +
    + + + +
    +
    +
    +

    Visit Us

    + + + +

    Location & Hours

    + + + +
    +
    + + +
    +

    Address

    + + + +

    456 Culinary Lane
    Downtown District, City 12345

    +
    +
    + + + +
    + + +
    +

    Hours

    + + + +

    Tue - Thu: 5:00 PM - 10:00 PM
    Fri - Sat: 5:00 PM - 11:00 PM
    Sun: 4:00 PM - 9:00 PM
    Monday: Closed

    +
    +
    + + + +
    + + +
    +

    Reservations

    + + + +

    (555) 987-6543

    +
    +
    +
    + + + + +
    + + + +
    +
    +
    +
    +
    + + + +
    +
    +

    Ready for an Unforgettable Evening?

    + + + +

    Reserve your table today and let us create a memorable dining experience for you and your guests.

    + + + + +
    +
    +', +); diff --git a/patterns/homepage/homepage-saas.php b/patterns/homepage/homepage-saas.php new file mode 100644 index 0000000..2ad5505 --- /dev/null +++ b/patterns/homepage/homepage-saas.php @@ -0,0 +1,491 @@ + __( 'SaaS / Startup Homepage', 'airo-wp' ), + 'categories' => array( 'airo-wp-homepage' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    +

    Scale Your Business
    with Powerful Tools

    + + + +

    The all-in-one platform that helps teams collaborate, automate workflows, and deliver results faster than ever before.

    + + + + + + + +

    No credit card required. 14-day free trial.

    +
    +
    +
    + + + +
    +

    Trusted by 10,000+ companies worldwide

    + + + +
    +

    Acme Corp

    + + + +

    TechFlow

    + + + +

    Innovate Inc

    + + + +

    DataStream

    + + + +

    CloudBase

    +
    +
    + + + +
    +
    +

    Everything You Need to Succeed

    + + + +

    Powerful features designed to help you work smarter, not harder.

    +
    + + + +
    +
    + + +

    Lightning Fast

    + + + +

    Optimized performance that keeps your team moving fast without slowdowns.

    +
    + + + +
    + + +

    Enterprise Security

    + + + +

    Bank-level encryption and compliance certifications to protect your data.

    +
    + + + +
    + + +

    Team Collaboration

    + + + +

    Real-time collaboration tools that keep your entire team in sync.

    +
    + + + +
    + + +

    Advanced Analytics

    + + + +

    Actionable insights and custom dashboards to track what matters.

    +
    + + + +
    + + +

    Workflow Automation

    + + + +

    Automate repetitive tasks and focus on what really matters.

    +
    + + + +
    + + +

    Global CDN

    + + + +

    Lightning-fast delivery from data centers around the world.

    +
    +
    +
    + + + +
    +
    +

    Trusted by Growing Companies

    + + + +

    See the impact we have made together

    +
    + + + +
    +
    0
    Active Users
    + + + +
    0
    Uptime SLA
    + + + +
    0
    Integrations
    + + + +
    0
    Support
    +
    +
    + + + +
    +
    +

    What Our Customers Say

    + + + +

    Join thousands of satisfied customers who have transformed their workflow.

    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "This platform completely transformed how our team collaborates. We have seen a 40% increase in productivity since switching."

    + + + +
    +
    Sarah Chen
    + + + +
    +

    Sarah Chen

    + + + +

    VP of Operations, TechCorp

    +
    +
    +
    + + + +
    +

    ★★★★★

    + + + +

    "The automation features alone have saved us countless hours every week. Best investment we have made this year."

    + + + +
    +
    Marcus Johnson
    + + + +
    +

    Marcus Johnson

    + + + +

    CEO, StartupFlow

    +
    +
    +
    + + + +
    +

    ★★★★★

    + + + +

    "Outstanding customer support and a product that just works. Our team adopted it instantly with zero learning curve."

    + + + +
    +
    Emily Rodriguez
    + + + +
    +

    Emily Rodriguez

    + + + +

    Director, Digital Agency

    +
    +
    +
    +
    +
    + + + +
    +
    +

    Simple, Transparent Pricing

    + + + +

    Choose the plan that fits your needs. Upgrade or downgrade anytime.

    +
    + + + +
    +
    +

    Starter

    + + + +

    Perfect for individuals and small teams getting started.

    + + + +

    $19/month

    + + + +
    + + + +
      +
    • Up to 5 team members
    • + + + +
    • 10GB storage
    • + + + +
    • Basic analytics
    • + + + +
    • Email support
    • +
    + + + +
    +
    + + + + + + + +
    +

    Enterprise

    + + + +

    For large organizations with advanced needs and custom requirements.

    + + + +

    $149/month

    + + + +
    + + + +
      +
    • Unlimited team members
    • + + + +
    • Unlimited storage
    • + + + +
    • Custom analytics
    • + + + +
    • 24/7 dedicated support
    • + + + +
    • SLA guarantee
    • +
    + + + +
    +
    +
    +
    + + + +
    +
    +

    Frequently Asked Questions

    + + + +

    Everything you need to know about our platform.

    +
    + + + +
    +
    + + + +
    + + + +
    +

    We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for annual enterprise plans. All payments are processed securely through Stripe.

    +
    + + + +
    + + + +
    +
    +
    + + + +
    +

    Ready to Get Started?

    + + + +

    Join thousands of teams already using our platform to work smarter.

    + + + + + + + +

    No credit card required. 14-day free trial.

    +
    +', +); diff --git a/patterns/modal/announcement-promo.php b/patterns/modal/announcement-promo.php new file mode 100644 index 0000000..ffc4574 --- /dev/null +++ b/patterns/modal/announcement-promo.php @@ -0,0 +1,29 @@ + __( 'Announcement & Promo Modal', 'airo-wp' ), + 'categories' => array( 'airo-wp-modal' ), + 'viewportWidth' => 1200, + 'content' => ' + +', +); diff --git a/patterns/modal/contact-form.php b/patterns/modal/contact-form.php new file mode 100644 index 0000000..d329a64 --- /dev/null +++ b/patterns/modal/contact-form.php @@ -0,0 +1,45 @@ + __( 'Contact Form Modal', 'airo-wp' ), + 'categories' => array( 'airo-wp-modal' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + + + + + +
    +', +); diff --git a/patterns/modal/modal-video-cta.php b/patterns/modal/modal-video-cta.php new file mode 100644 index 0000000..d93c842 --- /dev/null +++ b/patterns/modal/modal-video-cta.php @@ -0,0 +1,73 @@ + __( 'Video CTA Modal', 'airo-wp' ), + 'categories' => array( 'airo-wp-modal' ), + 'viewportWidth' => 1200, + 'content' => ' + + + + +
    +

    Click the button below to trigger the modal:

    + + + +
    +
    +', +); diff --git a/patterns/modal/newsletter-signup.php b/patterns/modal/newsletter-signup.php new file mode 100644 index 0000000..ae0d973 --- /dev/null +++ b/patterns/modal/newsletter-signup.php @@ -0,0 +1,37 @@ + __( 'Newsletter Signup Modal', 'airo-wp' ), + 'categories' => array( 'airo-wp-modal' ), + 'viewportWidth' => 1200, + 'content' => ' +
    + + + + + +
    +', +); diff --git a/patterns/pricing/pricing-cards.php b/patterns/pricing/pricing-cards.php new file mode 100644 index 0000000..3d55870 --- /dev/null +++ b/patterns/pricing/pricing-cards.php @@ -0,0 +1,47 @@ + __( 'Pricing Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Simple, Transparent Pricing

    + + + +

    Choose the plan that fits your needs. Upgrade or downgrade anytime.

    +
    + + + +
    +

    Starter

    $9/month

    Perfect for individuals and small projects getting started.

    + + + +
    Most Popular

    Professional

    $29/month

    For growing teams that need more power and flexibility.

    + + + +

    Enterprise

    $99/month

    For large organizations with advanced needs and support.

    +
    +
    +', +); diff --git a/patterns/pricing/pricing-comparison.php b/patterns/pricing/pricing-comparison.php new file mode 100644 index 0000000..4fe469c --- /dev/null +++ b/patterns/pricing/pricing-comparison.php @@ -0,0 +1,173 @@ + __( 'Pricing Comparison', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Choose Your Plan

    +
    + + + +
    + + + + + +
    +
    +', +); diff --git a/patterns/pricing/pricing-event-tickets.php b/patterns/pricing/pricing-event-tickets.php new file mode 100644 index 0000000..ed6a330 --- /dev/null +++ b/patterns/pricing/pricing-event-tickets.php @@ -0,0 +1,177 @@ + __( 'Event Ticket Pricing', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Tickets

    + + + +

    Secure Your Spot

    + + + +

    Early bird pricing available for a limited time. Choose the pass that fits your needs.

    +
    + + + +
    +
    +

    General Pass

    + + + +
    +

    $

    + + + +

    499

    +
    + + + +

    Early Bird Price

    + + + +
    + + + +
    +

    3-day conference access

    + + + +

    All keynotes & sessions

    + + + +

    Networking events

    + + + +

    Conference materials

    +
    + + + + +
    + + + +
    + + +

    VIP Pass

    + + + +
    +

    $

    + + + +

    899

    +
    + + + +

    Early Bird Price

    + + + +
    + + + +
    +

    Everything in General

    + + + +

    VIP lounge access

    + + + +

    Speaker meet & greet

    + + + +

    VIP dinner & awards

    + + + +

    Priority seating

    +
    + + + + +
    + + + +
    +

    Corporate Pass

    + + + +
    +

    $

    + + + +

    3,999

    +
    + + + +

    5 Team Members

    + + + +
    + + + +
    +

    5 VIP passes

    + + + +

    Dedicated account manager

    + + + +

    Private meeting room

    + + + +

    Logo on event materials

    +
    + + + + +
    +
    +
    +', +); diff --git a/patterns/pricing/pricing-membership-tiers.php b/patterns/pricing/pricing-membership-tiers.php new file mode 100644 index 0000000..ecf0016 --- /dev/null +++ b/patterns/pricing/pricing-membership-tiers.php @@ -0,0 +1,189 @@ + __( 'Membership Pricing Tiers', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Membership Plans

    + + + +

    Choose Your Plan

    +
    + + + +
    +
    +

    Basic

    + + + +

    Perfect for getting started

    + + + +
    +

    $

    + + + +

    29

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Gym access (6am-10pm)

    + + + +

    Basic equipment

    + + + +

    Locker room access

    + + + +

    Free WiFi

    +
    + + + + +
    + + + +
    + + +

    Premium

    + + + +

    Everything you need to succeed

    + + + +
    +

    $

    + + + +

    59

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    24/7 gym access

    + + + +

    All equipment & classes

    + + + +

    1 PT session/month

    + + + +

    Sauna & spa access

    + + + +

    Guest passes (2/month)

    +
    + + + + +
    + + + +
    +

    Elite

    + + + +

    The ultimate fitness experience

    + + + +
    +

    $

    + + + +

    99

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Everything in Premium

    + + + +

    4 PT sessions/month

    + + + +

    Nutrition coaching

    + + + +

    Recovery treatments

    + + + +

    Priority booking

    +
    + + + + +
    +
    +
    +', +); diff --git a/patterns/pricing/pricing-saas-plans.php b/patterns/pricing/pricing-saas-plans.php new file mode 100644 index 0000000..7dd6ddf --- /dev/null +++ b/patterns/pricing/pricing-saas-plans.php @@ -0,0 +1,169 @@ + __( 'SaaS Pricing Plans', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Pricing

    + + + +

    Simple, Transparent Pricing

    +
    + + + +
    +
    +

    Starter

    + + + +
    +

    $

    + + + +

    0

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Up to 3 team members

    + + + +

    Basic analytics

    + + + +

    1GB storage

    + + + +

    Community support

    +
    + + + + +
    + + + +
    + + +

    Pro

    + + + +
    +

    $

    + + + +

    29

    + + + +

    /month

    +
    + + + +
    + + + +
    +

    Unlimited team members

    + + + +

    Advanced analytics

    + + + +

    100GB storage

    + + + +

    Priority support

    + + + +

    API access

    +
    + + + + +
    + + + +
    +

    Enterprise

    + + + +
    +

    Custom

    +
    + + + +
    + + + +
    +

    Everything in Pro

    + + + +

    Custom integrations

    + + + +

    Unlimited storage

    + + + +

    Dedicated support

    + + + +

    SLA guarantee

    +
    + + + + +
    +
    +
    +', +); diff --git a/patterns/pricing/pricing-saas-tiers.php b/patterns/pricing/pricing-saas-tiers.php new file mode 100644 index 0000000..ff43fe5 --- /dev/null +++ b/patterns/pricing/pricing-saas-tiers.php @@ -0,0 +1,159 @@ + __( 'SaaS Pricing Tiers', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Simple, Transparent Pricing

    + + + +

    Choose the plan that fits your needs. Upgrade or downgrade anytime.

    +
    + + + +
    +
    +

    Starter

    + + + +

    Perfect for individuals and small teams getting started.

    + + + +

    $19/month

    + + + +
    + + + +
      +
    • Up to 5 team members
    • + + + +
    • 10GB storage
    • + + + +
    • Basic analytics
    • + + + +
    • Email support
    • +
    + + + +
    +
    + + + + + + + +
    +

    Enterprise

    + + + +

    For large organizations with advanced needs and custom requirements.

    + + + +

    $149/month

    + + + +
    + + + +
      +
    • Unlimited team members
    • + + + +
    • Unlimited storage
    • + + + +
    • Custom analytics
    • + + + +
    • 24/7 dedicated support
    • + + + +
    • SLA guarantee
    • +
    + + + +
    +
    +
    +
    +', +); diff --git a/patterns/pricing/pricing-tabs.php b/patterns/pricing/pricing-tabs.php new file mode 100644 index 0000000..ddbd1b2 --- /dev/null +++ b/patterns/pricing/pricing-tabs.php @@ -0,0 +1,399 @@ + __( 'Pricing Tabs', 'airo-wp' ), + 'categories' => array( 'airo-wp-pricing' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Pricing

    + + + +

    Choose Your Plan

    + + + +

    Save 20% with annual billing. All plans include a 14-day free trial.

    +
    + + + +
    + + + + + +
    +
    +', +); diff --git a/patterns/services/services-icon-cards.php b/patterns/services/services-icon-cards.php new file mode 100644 index 0000000..e814edb --- /dev/null +++ b/patterns/services/services-icon-cards.php @@ -0,0 +1,129 @@ + __( 'Services Icon Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-services' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    What We Offer

    + + + +

    Our Services

    + + + +

    Comprehensive solutions tailored to help your business grow and succeed in the digital landscape.

    +
    + + + +
    +
    + + +

    Web Development

    + + + +

    Custom websites and web applications built with modern technologies for optimal performance.

    + + + +
    +
    +

    Responsive Design

    +
    + + + +
    +

    SEO Optimized

    +
    + + + +
    +

    Fast Loading

    +
    +
    +
    + + + +
    + + +

    UI/UX Design

    + + + +

    Beautiful, intuitive interfaces designed with user experience at the forefront.

    + + + +
    +
    +

    User Research

    +
    + + + +
    +

    Wireframing

    +
    + + + +
    +

    Prototyping

    +
    +
    +
    + + + +
    + + +

    Digital Marketing

    + + + +

    Data-driven strategies to increase your online presence and drive conversions.

    + + + +
    +
    +

    SEO & SEM

    +
    + + + +
    +

    Social Media

    +
    + + + +
    +

    Email Campaigns

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/team/team-dark-minimal.php b/patterns/team/team-dark-minimal.php new file mode 100644 index 0000000..0de1525 --- /dev/null +++ b/patterns/team/team-dark-minimal.php @@ -0,0 +1,85 @@ + __( 'Team Grid Dark', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    The Team

    + + + +

    Meet the Creatives

    +
    + + + +
    +
    +
    Alex Rivera
    + + + +

    Alex Rivera

    + + + +

    Creative Director

    +
    + + + +
    +
    Jordan Kim
    + + + +

    Jordan Kim

    + + + +

    Lead Designer

    +
    + + + +
    +
    Marcus Chen
    + + + +

    Marcus Chen

    + + + +

    Strategy Director

    +
    + + + +
    +
    Sophie Taylor
    + + + +

    Sophie Taylor

    + + + +

    Marketing Lead

    +
    +
    +
    +', +); diff --git a/patterns/team/team-flip-cards.php b/patterns/team/team-flip-cards.php new file mode 100644 index 0000000..b277532 --- /dev/null +++ b/patterns/team/team-flip-cards.php @@ -0,0 +1,119 @@ + __( 'Team Flip Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Get to Know Us

    + + + +

    Hover over our team members to learn more

    +
    + + + +
    +
    +
    + +
    Chris Anderson
    + + +

    Chris Anderson

    + + +

    Product Manager

    + +
    + + + +
    + +

    Chris brings 10 years of product experience from leading tech companies. When not building products, you can find him hiking or playing guitar.

    + + + + +
    +
    + + + +
    +
    + +
    Maria Santos
    + + +

    Maria Santos

    + + +

    Lead Developer

    + +
    + + + +
    + +

    Maria is a React and WordPress expert who loves building accessible interfaces. She contributes to open source and mentors junior developers.

    + + + + +
    +
    + + + +
    +
    + +
    James Wright
    + + +

    James Wright

    + + +

    UX Designer

    + +
    + + + +
    + +

    James creates intuitive user experiences backed by research. He is passionate about accessibility and inclusive design practices.

    + + + + +
    +
    +
    +
    +', +); diff --git a/patterns/team/team-grid-social.php b/patterns/team/team-grid-social.php new file mode 100644 index 0000000..8a6ac7f --- /dev/null +++ b/patterns/team/team-grid-social.php @@ -0,0 +1,117 @@ + __( 'Team Grid with Social', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Meet Our Team

    + + + +

    The talented people behind our success

    +
    + + + +
    +
    +
    John Smith
    + + + +
    +

    John Smith

    + + + +

    CEO & Founder

    + + + + +
    +
    + + + +
    +
    Sarah Johnson
    + + + +
    +

    Sarah Johnson

    + + + +

    CTO

    + + + + +
    +
    + + + +
    +
    Mike Chen
    + + + +
    +

    Mike Chen

    + + + +

    Lead Designer

    + + + + +
    +
    + + + +
    +
    Emily Davis
    + + + +
    +

    Emily Davis

    + + + +

    Marketing Lead

    + + + + +
    +
    +
    +
    +', +); diff --git a/patterns/team/team-grid.php b/patterns/team/team-grid.php new file mode 100644 index 0000000..a2b9777 --- /dev/null +++ b/patterns/team/team-grid.php @@ -0,0 +1,45 @@ + __( 'Team Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Meet Our Team

    + + + +

    The talented people behind our success

    +
    + + + +
    +

    Alex Morgan

    CEO & Founder

    Visionary leader with 15+ years in tech startups.

    + + + +

    Jordan Lee

    CTO

    Full-stack architect passionate about performance.

    + + + +

    Sam Rivera

    Design Lead

    Award-winning designer focused on user experience.

    + + + +

    Taylor Kim

    Marketing Director

    Growth strategist driving brand awareness.

    +
    +
    +', +); diff --git a/patterns/team/team-image-accordion.php b/patterns/team/team-image-accordion.php new file mode 100644 index 0000000..9a7bbd2 --- /dev/null +++ b/patterns/team/team-image-accordion.php @@ -0,0 +1,79 @@ + __( 'Team Image Accordion', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Leadership Team

    + + + +

    Click on a team member to learn more about them

    +
    + + + +
    +
    +

    Robert Chen

    + + + +

    CEO

    +
    + + + +
    +

    Emma Williams

    + + + +

    COO

    +
    + + + +
    +

    David Kim

    + + + +

    CTO

    +
    + + + +
    +

    Sophie Brown

    + + + +

    CFO

    +
    + + + +
    +

    Michael Torres

    + + + +

    VP Engineering

    +
    +
    +
    +', +); diff --git a/patterns/team/team-instructor-profiles.php b/patterns/team/team-instructor-profiles.php new file mode 100644 index 0000000..c88e1c6 --- /dev/null +++ b/patterns/team/team-instructor-profiles.php @@ -0,0 +1,85 @@ + __( 'Instructor Profiles Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Expert Instructors

    + + + +

    Learn from Industry Leaders

    +
    + + + +
    +
    +
    Dr. Angela Yu
    + + + +

    Dr. Angela Yu

    + + + +

    Web Development

    +
    + + + +
    +
    Jose Portilla
    + + + +

    Jose Portilla

    + + + +

    Data Science

    +
    + + + +
    +
    Sarah Chen
    + + + +

    Sarah Chen

    + + + +

    UI/UX Design

    +
    + + + +
    +
    Michael Roberts
    + + + +

    Michael Roberts

    + + + +

    Marketing

    +
    +
    +
    +', +); diff --git a/patterns/team/team-leadership.php b/patterns/team/team-leadership.php new file mode 100644 index 0000000..5f38502 --- /dev/null +++ b/patterns/team/team-leadership.php @@ -0,0 +1,83 @@ + __( 'Leadership Team Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Our Leadership Team

    + + + +

    Experienced professionals dedicated to your success.

    +
    + + + +
    +
    +
    Robert Mitchell
    + + + +

    Robert Mitchell

    + + + +

    Managing Partner

    + + + +

    Harvard Law, 25+ years in corporate law and complex litigation.

    +
    + + + +
    +
    Sarah Williams
    + + + +

    Sarah Williams

    + + + +

    Senior Partner, Real Estate

    + + + +

    Yale Law, specializing in commercial real estate and development.

    +
    + + + +
    +
    Michael Chen
    + + + +

    Michael Chen

    + + + +

    Senior Partner, Tax Advisory

    + + + +

    Stanford Law, CPA with expertise in tax strategy and IRS matters.

    +
    +
    +
    +', +); diff --git a/patterns/team/team-speaker-showcase.php b/patterns/team/team-speaker-showcase.php new file mode 100644 index 0000000..d02a6b3 --- /dev/null +++ b/patterns/team/team-speaker-showcase.php @@ -0,0 +1,119 @@ + __( 'Speaker Showcase Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Featured Speakers

    + + + +

    Learn from Industry Leaders

    + + + +

    Hear from visionaries who are transforming industries and shaping the future of technology.

    +
    + + + +
    +
    +
    Speaker James Chen
    + + + +
    +

    James Chen

    + + + +

    CEO, TechVenture Labs

    + + + +

    AI & Machine Learning

    +
    +
    + + + +
    +
    Speaker Sarah Miller
    + + + +
    +

    Sarah Miller

    + + + +

    VP Engineering, CloudScale

    + + + +

    Cloud Architecture

    +
    +
    + + + +
    +
    Speaker Michael Roberts
    + + + +
    +

    Michael Roberts

    + + + +

    Founder, DataDriven

    + + + +

    Data Science

    +
    +
    + + + +
    +
    Speaker Emily Zhang
    + + + +
    +

    Emily Zhang

    + + + +

    CTO, SecureNet

    + + + +

    Cybersecurity

    +
    +
    +
    + + + + +
    +', +); diff --git a/patterns/team/team-trainer-profiles.php b/patterns/team/team-trainer-profiles.php new file mode 100644 index 0000000..fc3e545 --- /dev/null +++ b/patterns/team/team-trainer-profiles.php @@ -0,0 +1,85 @@ + __( 'Trainer Profiles Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-team' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Expert Trainers

    + + + +

    Meet Our Team

    +
    + + + +
    +
    +
    Trainer Alex
    + + + +

    Alex Rodriguez

    + + + +

    Head Trainer

    +
    + + + +
    +
    Trainer Maya
    + + + +

    Maya Johnson

    + + + +

    Yoga & Pilates

    +
    + + + +
    +
    Trainer Chris
    + + + +

    Chris Martinez

    + + + +

    Strength Coach

    +
    + + + +
    +
    Trainer Sarah
    + + + +

    Sarah Kim

    + + + +

    Cycling & HIIT

    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-cards-animated.php b/patterns/testimonials/testimonials-cards-animated.php new file mode 100644 index 0000000..2d08be2 --- /dev/null +++ b/patterns/testimonials/testimonials-cards-animated.php @@ -0,0 +1,135 @@ + __( 'Testimonials Cards Animated', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Testimonials

    + + + +

    What Our Clients Say

    + + + +

    Real feedback from real customers who have transformed their business.

    +
    + + + +
    +
    +
    + + + + + + + +
    + + + +

    "Working with this team was a game-changer. They delivered beyond our expectations and helped us achieve a 200% increase in conversions."

    + + + +
    +
    Client photo
    + + + +
    +

    Sarah Johnson

    + + + +

    CEO, TechStart Inc.

    +
    +
    +
    + + + +
    +
    + + + + + + + +
    + + + +

    "The attention to detail and commitment to excellence is unmatched. They truly understand our business needs and deliver solutions that work."

    + + + +
    +
    Client photo
    + + + +
    +

    Michael Chen

    + + + +

    Founder, GrowthLab

    +
    +
    +
    + + + +
    +
    + + + + + + + +
    + + + +

    "From concept to launch, the experience was seamless. Their team is responsive, creative, and delivers on time, every time."

    + + + +
    +
    Client photo
    + + + +
    +

    David Martinez

    + + + +

    CTO, InnovateCo

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-centered-slider.php b/patterns/testimonials/testimonials-centered-slider.php new file mode 100644 index 0000000..84bde7e --- /dev/null +++ b/patterns/testimonials/testimonials-centered-slider.php @@ -0,0 +1,73 @@ + __( 'Testimonials Centered Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Testimonials

    + + + +

    What Clients Say

    +
    + + + +
    +
    +
    +

    "They completely transformed our brand. The results exceeded our expectations, and our customers love the new direction."

    + + + +
    David Park
    + + + +
    +

    Joey Walsh

    + + + +

    CEO, Luxe Brands

    +
    +
    +
    + + + +
    +
    +

    "Working with this team was a game-changer for our startup. They understood our vision and brought it to life beautifully."

    + + + +
    David Park
    + + + +
    +

    David Park

    + + + +

    Founder, TechStart

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-client-grid.php b/patterns/testimonials/testimonials-client-grid.php new file mode 100644 index 0000000..c8cf13d --- /dev/null +++ b/patterns/testimonials/testimonials-client-grid.php @@ -0,0 +1,73 @@ + __( 'Client Testimonials Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Kind Words

    + + + +

    What Clients Say

    +
    + + + +
    +
    +

    "Sarah has an incredible ability to understand complex problems and translate them into elegant, user-friendly solutions. She transformed our product completely."

    + + + +
    +
    Jason Miller
    + + + +
    +

    Jason Miller

    + + + +

    CEO, TechVenture

    +
    +
    +
    + + + +
    +

    "Working with Sarah was a dream. She is not just a designer but a true partner who cares deeply about the success of your product. Highly recommended!"

    + + + +
    +
    Lisa Chen
    + + + +
    +

    Lisa Chen

    + + + +

    Product Lead, InnovateCo

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-customer-cards.php b/patterns/testimonials/testimonials-customer-cards.php new file mode 100644 index 0000000..cf27f07 --- /dev/null +++ b/patterns/testimonials/testimonials-customer-cards.php @@ -0,0 +1,107 @@ + __( 'Customer Testimonial Cards', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    What Our Customers Say

    + + + +

    Join thousands of satisfied customers who have transformed their workflow.

    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "This platform completely transformed how our team collaborates. We have seen a 40% increase in productivity since switching."

    + + + +
    +
    Sarah Chen
    + + + +
    +

    Sarah Chen

    + + + +

    VP of Operations, TechCorp

    +
    +
    +
    + + + +
    +

    ★★★★★

    + + + +

    "The automation features alone have saved us countless hours every week. Best investment we have made this year."

    + + + +
    +
    Marcus Johnson
    + + + +
    +

    Marcus Johnson

    + + + +

    CEO, StartupFlow

    +
    +
    +
    + + + +
    +

    ★★★★★

    + + + +

    "Outstanding customer support and a product that just works. Our team adopted it instantly with zero learning curve."

    + + + +
    +
    Emily Rodriguez
    + + + +
    +

    Emily Rodriguez

    + + + +

    Director, Digital Agency

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-grid.php b/patterns/testimonials/testimonials-grid.php new file mode 100644 index 0000000..7adfb35 --- /dev/null +++ b/patterns/testimonials/testimonials-grid.php @@ -0,0 +1,137 @@ + __( 'Testimonials Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Trusted by Industry Leaders

    +
    + + + +
    +
    +
    +
    James Wilson
    + + + +
    +

    James Wilson

    + + + +

    CTO, StartupHub

    +
    +
    + + + +

    "The performance gains we have seen since switching to airo-wp are remarkable. Our Core Web Vitals scores improved significantly."

    + + + +
    +

    ★★★★★

    +
    +
    + + + +
    +
    +
    Lisa Thompson
    + + + +
    +

    Lisa Thompson

    + + + +

    Marketing Director, BrandCo

    +
    +
    + + + +

    "The patterns saved us weeks of design work. We launched our new site in record time with professional-looking sections."

    + + + +
    +

    ★★★★★

    +
    +
    + + + +
    +
    +
    David Park
    + + + +
    +

    David Park

    + + + +

    Agency Owner, WebCraft

    +
    +
    + + + +

    "Our team loves how easy it is to customize. The blocks work perfectly with our existing theme and match our brand guidelines."

    + + + +
    +

    ★★★★★

    +
    +
    + + + +
    +
    +
    Amanda Foster
    + + + +
    +

    Amanda Foster

    + + + +

    Designer, CreativeStudio

    +
    +
    + + + +

    "Finally, a block library that understands design! The attention to spacing, typography, and visual hierarchy is exceptional."

    + + + +
    +

    ★★★★★

    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-guest-reviews.php b/patterns/testimonials/testimonials-guest-reviews.php new file mode 100644 index 0000000..357a9e8 --- /dev/null +++ b/patterns/testimonials/testimonials-guest-reviews.php @@ -0,0 +1,71 @@ + __( 'Guest Reviews Section', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Reviews

    + + + +

    What Our Guests Say

    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "An unforgettable dining experience! The lobster risotto was pure perfection. Will definitely be back."

    + + + +

    - Michael R., via Google

    +
    + + + +
    +

    ★★★★★

    + + + +

    "We celebrated our anniversary here and it was magical. The service was impeccable and the food was outstanding."

    + + + +

    - Sarah T., via Yelp

    +
    + + + +
    +

    ★★★★★

    + + + +

    "Best restaurant in the city, hands down. The chef truly understands how to bring out the best in each ingredient."

    + + + +

    - James L., via TripAdvisor

    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-luxury-slider.php b/patterns/testimonials/testimonials-luxury-slider.php new file mode 100644 index 0000000..aad96b0 --- /dev/null +++ b/patterns/testimonials/testimonials-luxury-slider.php @@ -0,0 +1,101 @@ + __( 'Luxury Testimonials Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Testimonials

    + + + +

    What Our Clients Say

    +
    + + + +
    +
    +
    +

    "The team made finding our dream home an absolute pleasure. Their knowledge of the luxury market is unmatched, and they found us a property that exceeded all our expectations."

    + + + +
    +
    Client photo
    + + + +
    +

    Michael Richardson

    + + + +

    Beverly Hills Buyer

    +
    +
    +
    +
    + + + +
    +
    +

    "Selling our estate was a seamless experience. They positioned our property perfectly in the market and attracted qualified buyers from around the world. Truly exceptional service."

    + + + +
    +
    Client photo
    + + + +
    +

    Sarah Chen

    + + + +

    Bel Air Seller

    +
    +
    +
    +
    + + + +
    +
    +

    "As international buyers, we relied heavily on their expertise. They navigated complex legal requirements and found us a stunning oceanfront property that serves as our perfect retreat."

    + + + +
    +
    Client photo
    + + + +
    +

    David Laurent

    + + + +

    International Buyer

    +
    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-professional-grid.php b/patterns/testimonials/testimonials-professional-grid.php new file mode 100644 index 0000000..64ebdc4 --- /dev/null +++ b/patterns/testimonials/testimonials-professional-grid.php @@ -0,0 +1,69 @@ + __( 'Professional Testimonials Grid', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    What Our Clients Say

    +
    + + + +
    +
    +

    "They handled our company merger with exceptional skill and attention to detail. Their guidance was invaluable throughout the entire process."

    + + + +
    +
    Thomas Anderson
    + + + +
    +

    Thomas Anderson

    + + + +

    CEO, Anderson Industries

    +
    +
    +
    + + + +
    +

    "Professional, responsive, and truly invested in our success. They have been our trusted advisors for over a decade now."

    + + + +
    +
    Patricia Reynolds
    + + + +
    +

    Patricia Reynolds

    + + + +

    Managing Director, Reynolds Group

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-rating-cards.php b/patterns/testimonials/testimonials-rating-cards.php new file mode 100644 index 0000000..d9e1322 --- /dev/null +++ b/patterns/testimonials/testimonials-rating-cards.php @@ -0,0 +1,131 @@ + __( 'Testimonials with Star Ratings', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    What Our Customers Say

    + + + +

    Real reviews from real customers

    +
    + + + +
    +
    +
    + + + + + + + +
    + + + +

    "This product has completely transformed how we work. The interface is intuitive and the features are exactly what we needed. Highly recommended!"

    + + + +
    +
    Amanda Peters
    + + + +
    +

    Amanda Peters

    + + + +

    Marketing Director

    +
    +
    +
    + + + +
    +
    + + + + + + + +
    + + + +

    "The customer support is outstanding. Every time I had a question, they responded quickly and resolved my issues efficiently. Great experience overall."

    + + + +
    +
    David Kim
    + + + +
    +

    David Kim

    + + + +

    Software Engineer

    +
    +
    +
    + + + +
    +
    + + + + + + + +
    + + + +

    "We have seen a 40% increase in productivity since implementing this solution. The ROI has been incredible and the team loves using it daily."

    + + + +
    +
    Sarah Mitchell
    + + + +
    +

    Sarah Mitchell

    + + + +

    Operations Manager

    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-slider.php b/patterns/testimonials/testimonials-slider.php new file mode 100644 index 0000000..97abf3b --- /dev/null +++ b/patterns/testimonials/testimonials-slider.php @@ -0,0 +1,113 @@ + __( 'Testimonials Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    What Our Customers Say

    + + + +

    Join thousands of satisfied users building amazing websites

    +
    + + + +
    +
    +
    +

    "

    + + + +

    airo-wp has completely transformed how we build websites. The blocks are incredibly flexible and the performance is outstanding. Our development time has been cut in half!

    + + + +
    +
    Sarah Johnson
    + + + +
    +

    Sarah Johnson

    + + + +

    Lead Developer, TechCorp

    +
    +
    +
    +
    + + + +
    +
    +

    "

    + + + +

    The accessibility features are top-notch. As someone who focuses on inclusive design, I appreciate how every component follows WCAG guidelines out of the box.

    + + + +
    +
    Michael Chen
    + + + +
    +

    Michael Chen

    + + + +

    UX Designer, AccessibleWeb

    +
    +
    +
    +
    + + + +
    +
    +

    "

    + + + +

    I have tried many block libraries, but airo-wp stands out for its attention to detail and performance. The slider and accordion blocks are particularly impressive!

    + + + +
    +
    Emily Rodriguez
    + + + +
    +

    Emily Rodriguez

    + + + +

    Freelance Web Developer

    +
    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-star-slider.php b/patterns/testimonials/testimonials-star-slider.php new file mode 100644 index 0000000..1461415 --- /dev/null +++ b/patterns/testimonials/testimonials-star-slider.php @@ -0,0 +1,93 @@ + __( 'Star Rating Testimonial Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Customer Love

    + + + +

    What Our Customers Say

    +
    + + + +
    +
    +
    +

    ★★★★★

    + + + +

    "Absolutely love the quality! The sneakers are so comfortable and stylish. Already planning my next purchase."

    + + + +

    — Emma S.

    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "Fast shipping and excellent customer service. The watch exceeded my expectations. Highly recommend!"

    + + + +

    — Michael R.

    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "The leather backpack is gorgeous and so well-made. Perfect for work and weekend trips alike."

    + + + +

    — Sarah L.

    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "Best online shopping experience I have had. The quality is unmatched and the prices are fair."

    + + + +

    — David K.

    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-student-slider.php b/patterns/testimonials/testimonials-student-slider.php new file mode 100644 index 0000000..15a0dbb --- /dev/null +++ b/patterns/testimonials/testimonials-student-slider.php @@ -0,0 +1,113 @@ + __( 'Student Testimonials Slider', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +

    Student Success

    + + + +

    What Our Students Say

    +
    + + + +
    +
    +
    +

    ★★★★★

    + + + +

    "This platform completely changed my career. I went from knowing nothing about coding to landing my first developer job in just 6 months. The instructors are amazing!"

    + + + +
    +
    Alex
    + + + +
    +

    Alex Thompson

    + + + +

    Frontend Developer at Google

    +
    +
    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "The data science course was incredibly comprehensive. I learned Python, machine learning, and got hands-on experience with real projects. Worth every penny!"

    + + + +
    +
    Emily
    + + + +
    +

    Emily Rodriguez

    + + + +

    Data Analyst at Amazon

    +
    +
    +
    +
    + + + +
    +
    +

    ★★★★★

    + + + +

    "As someone who switched careers at 35, these courses gave me the confidence and skills I needed. Now I am a senior UX designer. It is never too late to learn!"

    + + + +
    +
    Marcus
    + + + +
    +

    Marcus Johnson

    + + + +

    Senior UX Designer at Microsoft

    +
    +
    +
    +
    +
    +
    +', +); diff --git a/patterns/testimonials/testimonials-video.php b/patterns/testimonials/testimonials-video.php new file mode 100644 index 0000000..e75dbb3 --- /dev/null +++ b/patterns/testimonials/testimonials-video.php @@ -0,0 +1,61 @@ + __( 'Video Testimonial', 'airo-wp' ), + 'categories' => array( 'airo-wp-testimonials' ), + 'viewportWidth' => 1200, + 'content' => ' +
    +
    +
    + +
    + + + +
    + + +

    Working with this team transformed our business. Their innovative approach and dedication to excellence helped us achieve results we never thought possible.

    + + + +
    + + + +
    +
    Robert Chen
    + + + +
    +

    Robert Chen

    + + + +

    CEO, TechStart Inc.

    +
    +
    +
    +
    +
    + + + + +', +); diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..f375c41 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,65 @@ + + + PHPCS for airo-wp (WPCS + VIP-Go + PHPCompatibility) + + + + + + airo-wp.php + functions + includes + + vendor/* + dependencies/* + bin/* + includes/Internal/Testing/* + + + + includes/Internal/DependencyManagement/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f3c6c9a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + tests + + + + + includes + airo-wp.php + + + vendor + dependencies + + + diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..0664daf --- /dev/null +++ b/readme.txt @@ -0,0 +1,133 @@ +=== Airo WP AI Builder === +Contributors: godaddy +Tags: airo, godaddy, mcp, ai, block-patterns +Requires at least: 6.9 +Tested up to: 7.0 +Requires PHP: 7.4 +Stable tag: 0.2.4 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +MCP server and block pattern library for AI-powered site building. Connect any AI client to manage content and assemble pages from curated patterns. + +== Description == + +**Airo WP AI Builder** is a WordPress plugin that exposes your site to AI assistants via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). It also bundles a curated set of Gutenberg block patterns built on the Twenty Twenty-Five (tt5) theme, giving any connected AI a ready-made vocabulary for assembling pages. + +**MCP Server** + +Registers an MCP-compatible server endpoint that any AI client can connect to. Tools cover posts, pages, media, templates, navigation menus, and global styles. + +**Block Patterns** + +A curated library of Gutenberg block patterns designed for the Twenty Twenty-Five theme. Patterns are sourced from the DesignSetGo project and automatically deferred when DesignSetGo is active to avoid duplication. + +**AI-agnostic** + +No specific AI is bundled or required. Connect Claude, GPT, Gemini, or any MCP-compatible assistant of your choice. + +**Developer highlights:** + +* Custom DI container with domain `Package` classes registered on `plugins_loaded` +* Strauss-prefixed vendor code in `dependencies/` to avoid conflicts +* PHPUnit unit tests with Brain Monkey +* PHP 7.4 through 8.3 compatibility + +== Roadmap (v1) == + +The following features are planned for the v1 release: + +* **Site generation** — build a complete WordPress site from a single prompt +* **Conversational setup** — add pages, swap images, and update layouts through plain-English chat +* **WooCommerce storefront** — auto-configure a storefront with AI-generated products from a single prompt + +== Installation == + +1. Upload the `airo-wp` folder to `/wp-content/plugins/`, or install from your organization's deployment pipeline. +2. Activate the plugin through the **Plugins** screen in WordPress. +3. For development, run `composer install` in the plugin directory before activation (see CONTRIBUTORS.md). + +== Frequently Asked Questions == + += What does Airo WP AI Builder do right now? = + +It registers an MCP server on your WordPress site and provides a block pattern library based on the Twenty Twenty-Five theme. You connect an AI client of your choice to the MCP endpoint and use it to read and write site content. + += Which AI can I use? = + +Any MCP-compatible AI client — Claude, GPT, Gemini, or others. The plugin does not bundle or require a specific AI. + += Where do the block patterns come from? = + +Patterns are sourced from the [DesignSetGo plugin](https://wordpress.org/plugins/designsetgo/). If DesignSetGo is active, Airo WP AI Builder defers to it automatically to avoid duplication. + += What PHP versions are supported? = + +PHP 7.4 is the minimum. PHP 8.3 is the recommended version for local development. CI validates 7.4, 8.0, 8.1, 8.2, and 8.3. + += Why are dependencies in a `dependencies/` folder? = + +Runtime Composer packages are namespace-prefixed with Strauss into `dependencies/` so they do not clash with other plugins' autoloaders. + +== Changelog == + += 0.2.5 = +* Renamed plugin to "Airo AI Builder" + += 0.2.4 = +* Bundled block dynamic tags so shipped blocks resolve dynamic content correctly +* Corrected block class renames, docblocks, and patch paths in synced block source + += 0.2.3 = +* Renamed plugin to "Airo WP AI Builder" (WordPress.org trademark compliance) +* Fixed PHP syntax error: ABSPATH guard now placed after namespace declaration +* Fixed inline style in form submission meta box to use wp_add_inline_style() +* Scrubbed remote image URLs (Unsplash, picsum) from block patterns and src/blocks +* Corrected Plugin URI and Description header in plugin file +* Added external services documentation for Cloudflare Turnstile and Google Maps + += 0.2.2 = +* Fixed readme short description exceeding the 150-character limit + += 0.2.1 = +* Renamed plugin display name to "Airo WP" + += 0.2.0 = +* Added MCP server with tools for posts, pages, media, templates, navigation, and global styles +* Added DesignSetGo blocks and patterns with automated sync pipeline +* Added website management tools (site info, plugins, themes) +* Added release tooling with version bump, build pipeline, and distributable zip packaging +* E2e and plugin-check tests now validate the distributable build artifact + += 0.1.0 = +* Initial scaffold: bootstrap, DI container, package loader, Strauss build, unit tests, and PHPCS (WPCS + VIP-Go). + +== Upgrade Notice == + += 0.1.0 = +Initial release. Safe to install for development and future Airo features. + +== External Services == + +This plugin optionally connects to two third-party services depending on which blocks are used. + +**Cloudflare Turnstile** (form spam protection) + +Used by the Form block when a Turnstile site key is configured under Settings → Airo WP AI Builder → Integrations. +When a visitor loads a page containing a form block, the Turnstile JavaScript widget is fetched from +`https://challenges.cloudflare.com/`. On form submission, the CAPTCHA token is verified by sending it +to `https://challenges.cloudflare.com/turnstile/v0/siteverify` along with the visitor's IP address. +No data is sent if no Turnstile site key is configured. + +- Terms of Service: https://www.cloudflare.com/website-terms/ +- Privacy Policy: https://www.cloudflare.com/privacypolicy/ + +**Google Maps** (map embed block) + +Used by the Map block when a Google Maps API key is configured. +When a visitor loads a page containing a map block, the Google Maps JavaScript API is loaded from +`https://maps.googleapis.com/`. The visitor's browser communicates directly with Google Maps servers. +No data is sent if no API key is configured. + +- Terms of Service: https://cloud.google.com/maps-platform/terms +- Privacy Policy: https://policies.google.com/privacy diff --git a/src/blocks/accordion-item/block.json b/src/blocks/accordion-item/block.json new file mode 100644 index 0000000..b32a69e --- /dev/null +++ b/src/blocks/accordion-item/block.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/accordion-item", + "version": "1.0.0", + "title": "Accordion Item", + "category": "airo-wp", + "parent": [ + "airo-wp/accordion" + ], + "description": "An individual collapsible item within an accordion.", + "keywords": [ + "accordion", + "item", + "panel", + "section" + ], + "textdomain": "airo-wp", + "icon": "menu-alt2", + "attributes": { + "title": { + "type": "string", + "default": "Accordion Item" + }, + "isOpen": { + "type": "boolean", + "default": false + }, + "uniqueId": { + "type": "string", + "default": "" + } + }, + "usesContext": [ + "airo-wp/accordion/allowMultipleOpen", + "airo-wp/accordion/iconStyle", + "airo-wp/accordion/iconPosition", + "airo-wp/accordion/openBackgroundColor", + "airo-wp/accordion/openTextColor", + "airo-wp/accordion/hoverBackgroundColor", + "airo-wp/accordion/hoverTextColor" + ], + "example": { + "attributes": { + "title": "Frequently Asked Question", + "isOpen": false + }, + "innerBlocks": [ + { + "name": "core/paragraph", + "attributes": { + "content": "This is the answer to the frequently asked question. It can contain any block content including text, images, lists, and more." + } + } + ] + }, + "supports": { + "html": false, + "reusable": false, + "spacing": { + "padding": true, + "margin": false, + "__experimentalDefaultControls": { + "padding": true + } + }, + "color": { + "background": true, + "text": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "background": { + "backgroundImage": true, + "backgroundSize": true, + "backgroundPosition": true, + "backgroundRepeat": true, + "__experimentalDefaultControls": { + "backgroundImage": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true, + "lineHeight": true, + "fontWeight": true + }, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true + } + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/accordion-item/edit.js b/src/blocks/accordion-item/edit.js new file mode 100644 index 0000000..15048fa --- /dev/null +++ b/src/blocks/accordion-item/edit.js @@ -0,0 +1,249 @@ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + RichText, + InspectorControls, +} from '@wordpress/block-editor'; +import { useEffect, useMemo } from '@wordpress/element'; +import { Icon, ToggleControl } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useSelect, useDispatch } from '@wordpress/data'; +import classnames from 'classnames'; + +// Icon components for different styles +const ChevronIcon = () => ( + + + +); + +const PlusMinusIcon = ({ isOpen }) => ( + + {isOpen ? ( + + ) : ( + <> + + + )} + +); + +const CaretIcon = () => ( + + + +); + +export default function AccordionItemEdit({ + attributes, + setAttributes, + context, + clientId, +}) { + const { title, isOpen, uniqueId } = attributes; + + // Get context from parent accordion + const iconStyle = context['airo-wp/accordion/iconStyle'] || 'chevron'; + const iconPosition = + context['airo-wp/accordion/iconPosition'] || 'right'; + const allowMultipleOpen = + context['airo-wp/accordion/allowMultipleOpen'] || false; + + const { updateBlockAttributes } = useDispatch('core/block-editor'); + + // Get sibling accordion IDs as a string for stable comparison + const { siblingsString } = useSelect( + (select) => { + const { getBlockParents, getBlockOrder, getBlockName } = + select('core/block-editor'); + const parents = getBlockParents(clientId); + const parentId = parents[parents.length - 1]; + + if (!parentId) { + return { siblingsString: '' }; + } + + // Get all child block IDs and filter to accordion items + const childOrder = getBlockOrder(parentId); + const accordionSiblings = childOrder.filter( + (id) => + id !== clientId && + getBlockName(id) === 'airo-wp/accordion-item' + ); + + return { + // Join to string for stable value comparison + siblingsString: accordionSiblings.join(','), + }; + }, + [clientId] + ); + + // Parse string back to array with useMemo for stable reference + const siblingClientIds = useMemo( + () => (siblingsString ? siblingsString.split(',') : []), + [siblingsString] + ); + + // Handle accordion item click + const handleToggle = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Toggle current item + const newIsOpen = !isOpen; + setAttributes({ isOpen: newIsOpen }); + + // If opening and single mode, close all siblings + if (newIsOpen && !allowMultipleOpen) { + siblingClientIds.forEach((siblingId) => { + updateBlockAttributes(siblingId, { isOpen: false }); + }); + } + }; + + // Generate unique ID for accessibility + useEffect(() => { + if (!uniqueId) { + setAttributes({ + uniqueId: `accordion-item-${Math.random().toString(36).substr(2, 9)}`, + }); + } + }, [uniqueId, setAttributes]); + + // Declaratively calculate classes + const itemClasses = classnames('airo-wp-accordion-item', { + 'airo-wp-accordion-item--open': isOpen, + 'airo-wp-accordion-item--closed': !isOpen, + }); + + const blockProps = useBlockProps({ + className: itemClasses, + }); + + // Inner blocks for accordion content + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-accordion-item__content', + }, + { + template: [ + [ + 'core/paragraph', + { + placeholder: __('Add content…', 'airo-wp'), + }, + ], + ], + } + ); + + // Render the appropriate icon + const renderIcon = () => { + if (iconStyle === 'none') { + return null; + } + + let IconComponent; + switch (iconStyle) { + case 'plus-minus': + IconComponent = () => ; + break; + case 'caret': + IconComponent = CaretIcon; + break; + case 'chevron': + default: + IconComponent = ChevronIcon; + } + + return ( + + ); + }; + + return ( + <> + + setAttributes({ isOpen: false })} + > + isOpen !== false} + onDeselect={() => setAttributes({ isOpen: false })} + isShownByDefault + > + + setAttributes({ isOpen: value }) + } + __nextHasNoMarginBottom + /> + + + + +
    +
    + +
    + +
    +
    +
    +
    + + ); +} diff --git a/src/blocks/accordion-item/editor.scss b/src/blocks/accordion-item/editor.scss new file mode 100644 index 0000000..1337e6e --- /dev/null +++ b/src/blocks/accordion-item/editor.scss @@ -0,0 +1,89 @@ +/** + * Accordion Item Block - Editor Styles + * Import frontend styles + editor-specific overrides + */ + +// Import all frontend styles so they appear in editor +@use './style.scss' as *; + +// Editor-specific overrides and enhancements +.editor-styles-wrapper, +.block-editor-block-list__layout { + + .airo-wp-accordion-item { + // Editor-specific spacing between items + &:not(:last-child) { + margin-bottom: var(--wp--preset--spacing--20, 0.5rem); + } + + // Panel behavior in editor - interactive like frontend + .airo-wp-accordion-item__panel { + // Respect open/closed state in editor + // But disable animation for instant toggle + transition: none !important; + + // Add visual indicator in editor + .airo-wp-accordion-item__content { + // Subtle border in editor to show content area + border-left: 2px solid var(--wp--preset--color--primary, rgba(37, 99, 235, 0.2)); + margin-left: var(--wp--preset--spacing--40, 1.5rem); + padding-left: var(--wp--preset--spacing--30, 1rem); + } + } + + // Editor visual feedback when selected + &.is-selected { + + > .airo-wp-accordion-item__header { + + .airo-wp-accordion-item__trigger { + outline: 2px solid var(--wp--admin-theme-color, #007cba); + outline-offset: 2px; + box-shadow: 0 0 0 4px rgba(0, 124, 186, 0.1); + } + } + } + + // Make trigger interactive in editor + .airo-wp-accordion-item__trigger { + // Allow trigger to be clickable + cursor: pointer; + + // Title is editable via RichText + .airo-wp-accordion-item__title { + cursor: text; + } + + // Show hover effects in editor + &:hover { + background-color: var(--airo-wp-accordion-hover-bg, var(--wp--preset--color--base-2, rgba(0, 0, 0, 0.04))); + } + } + + // Visual feedback for open/closed state in editor + &.airo-wp-accordion-item--open { + // Panel is visible when open + .airo-wp-accordion-item__panel { + display: block; + } + } + + &.airo-wp-accordion-item--closed { + // Panel is hidden when closed + .airo-wp-accordion-item__panel { + display: none; + } + } + } +} + +// Block preview (inserter) +.block-editor-block-preview__content { + + .airo-wp-accordion-item { + + .airo-wp-accordion-item__panel { + display: block !important; + } + } +} diff --git a/src/blocks/accordion-item/index.js b/src/blocks/accordion-item/index.js new file mode 100644 index 0000000..9904f6d --- /dev/null +++ b/src/blocks/accordion-item/index.js @@ -0,0 +1,31 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './edit'; +import Save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +// Import styles +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + ), + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, +}); diff --git a/src/blocks/accordion-item/save.js b/src/blocks/accordion-item/save.js new file mode 100644 index 0000000..aa71edc --- /dev/null +++ b/src/blocks/accordion-item/save.js @@ -0,0 +1,126 @@ +import { + useBlockProps, + useInnerBlocksProps, + RichText, +} from '@wordpress/block-editor'; +import classnames from 'classnames'; + +// Icon SVGs - same as edit.js +const ChevronIcon = () => ( + + + +); + +const PlusMinusIcon = ({ isOpen }) => ( + + {isOpen ? ( + + ) : ( + <> + + + )} + +); + +const CaretIcon = () => ( + + + +); + +export default function AccordionItemSave({ attributes, context }) { + const { title, isOpen, uniqueId } = attributes; + + // Get context from parent accordion (same as edit.js) + const iconStyle = context?.['airo-wp/accordion/iconStyle'] || 'chevron'; + const iconPosition = + context?.['airo-wp/accordion/iconPosition'] || 'right'; + + // Same classes as edit.js - MUST MATCH + const itemClasses = classnames('airo-wp-accordion-item', { + 'airo-wp-accordion-item--open': isOpen, + 'airo-wp-accordion-item--closed': !isOpen, + }); + + const blockProps = useBlockProps.save({ + className: itemClasses, + 'data-initially-open': isOpen, + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-accordion-item__content', + }); + + // Render icon (same logic as edit.js) + const renderIcon = () => { + if (iconStyle === 'none') { + return null; + } + + let IconComponent; + switch (iconStyle) { + case 'plus-minus': + IconComponent = () => ; + break; + case 'caret': + IconComponent = CaretIcon; + break; + case 'chevron': + default: + IconComponent = ChevronIcon; + } + + return ( + + ); + }; + + const headerId = `${uniqueId}-header`; + const panelId = `${uniqueId}-panel`; + + return ( +
    +
    + +
    + + + ); +} diff --git a/src/blocks/accordion-item/style.scss b/src/blocks/accordion-item/style.scss new file mode 100644 index 0000000..c24b85a --- /dev/null +++ b/src/blocks/accordion-item/style.scss @@ -0,0 +1,221 @@ +/** + * Accordion Item Block - Frontend Styles + * Enhanced UI/UX with smooth animations and better visual feedback + */ + +.airo-wp-accordion-item { + // Spacing between accordion items + margin: 0; + padding: 0; + position: relative; + + // Smooth transition for all state changes + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + // Header container + .airo-wp-accordion-item__header { + margin: 0; + position: relative; + } + + // Trigger button - Enhanced UX + .airo-wp-accordion-item__trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + // Increased padding for better touch targets (min 44px height) + padding: var(--wp--preset--spacing--40, 1.25rem) var(--wp--preset--spacing--40, 1.5rem); + background: transparent; + border: none; + cursor: pointer; + text-align: left; + font-family: inherit; + font-size: inherit; + font-weight: 600; + line-height: 1.6; + color: inherit; + // Smooth transitions with better easing + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 0; + position: relative; + + // Subtle hover effect + &:hover { + // Use parent accordion's hover colors if set, otherwise default + background-color: var(--airo-wp-accordion-hover-bg, var(--wp--preset--color--base-2, rgba(0, 0, 0, 0.04))); + color: var(--airo-wp-accordion-hover-text, inherit); + + // Icon scale on hover + .airo-wp-accordion-item__icon { + transform: scale(1.1); + color: var(--airo-wp-accordion-hover-text, var(--wp--preset--color--contrast, currentColor)); + } + } + + &:focus, + &:focus-visible { + outline: none; + } + + // Active state (pressed) + &:active { + transform: scale(0.99); + background-color: var(--wp--preset--color--base-2, rgba(0, 0, 0, 0.08)); + } + + // Icon positioning + &.airo-wp-accordion-item__trigger--icon-left { + flex-direction: row; + + .airo-wp-accordion-item__icon { + margin-right: var(--wp--preset--spacing--30, 0.75rem); + order: -1; + } + } + + &.airo-wp-accordion-item__trigger--icon-right { + flex-direction: row; + + .airo-wp-accordion-item__icon { + margin-left: var(--wp--preset--spacing--30, 0.75rem); + order: 1; + } + } + } + + // Title text + .airo-wp-accordion-item__title { + flex: 1; + // Better text rendering + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + // Icon - Enhanced animations + .airo-wp-accordion-item__icon { + display: flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + // Smooth icon transitions with better easing + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); + flex-shrink: 0; + opacity: 0.7; + + svg { + width: 100%; + height: 100%; + } + } + + // Open state - Enhanced visual feedback with custom colors + &.airo-wp-accordion-item--open { + // Use parent accordion's open colors if set, otherwise default + background-color: var(--airo-wp-accordion-open-bg, var(--wp--preset--color--base-2, rgba(0, 0, 0, 0.02))); + color: var(--airo-wp-accordion-open-text, inherit); + + // Optional: subtle shadow for depth + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .airo-wp-accordion-item__trigger { + // Apply open colors to trigger + background-color: var(--airo-wp-accordion-open-bg, transparent); + color: var(--airo-wp-accordion-open-text, inherit); + + &:hover { + // Hover state when open - use hover colors (which default to open colors) + background-color: var(--airo-wp-accordion-hover-bg, var(--wp--preset--color--base-2, rgba(0, 0, 0, 0.03))); + color: var(--airo-wp-accordion-hover-text, inherit); + } + } + + // Rotate and enhance icon when open + .airo-wp-accordion-item__icon { + transform: rotate(180deg); + opacity: 1; + color: var(--airo-wp-accordion-open-text, var(--wp--preset--color--contrast, currentColor)); + } + + // Ensure panel is visible when open + .airo-wp-accordion-item__panel { + opacity: 1; + } + } + + // Content panel - Smoother animations + .airo-wp-accordion-item__panel { + overflow: hidden; + // Enhanced transition with better easing + transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1); + + // Hidden state (via frontend.js) + &[hidden] { + display: none; + } + + // Content wrapper with improved spacing + .airo-wp-accordion-item__content { + padding: 0 var(--wp--preset--spacing--40, 1.5rem) var(--wp--preset--spacing--40, 1.5rem); + + // Add subtle top spacing + padding-top: var(--wp--preset--spacing--20, 0.5rem); + + // Remove top margin from first child + > *:first-child { + margin-top: 0; + } + + // Remove bottom margin from last child + > *:last-child { + margin-bottom: 0; + } + } + } + + // Closed state + &.airo-wp-accordion-item--closed { + background-color: transparent; + box-shadow: none; + + .airo-wp-accordion-item__panel { + opacity: 0; + } + } +} + +// Mobile optimizations +@media (max-width: 768px) { + + .airo-wp-accordion-item { + + .airo-wp-accordion-item__trigger { + // Ensure minimum touch target size + min-height: 44px; + padding: var(--wp--preset--spacing--40, 1rem); + } + + .airo-wp-accordion-item__panel .airo-wp-accordion-item__content { + padding-left: var(--wp--preset--spacing--40, 1rem); + padding-right: var(--wp--preset--spacing--40, 1rem); + } + } +} + +// Print styles - show all content +@media print { + + .airo-wp-accordion-item { + + .airo-wp-accordion-item__panel { + display: block !important; + height: auto !important; + } + + .airo-wp-accordion-item__icon { + display: none; + } + } +} diff --git a/src/blocks/accordion/block.json b/src/blocks/accordion/block.json new file mode 100644 index 0000000..14221d3 --- /dev/null +++ b/src/blocks/accordion/block.json @@ -0,0 +1,209 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/accordion", + "version": "1.0.0", + "title": "Accordion", + "category": "airo-wp", + "description": "Create accessible accordion sections with customizable icons and behavior for FAQs and content organization.", + "keywords": [ + "accordion", + "faq", + "collapsible", + "toggle", + "expand" + ], + "textdomain": "airo-wp", + "icon": "menu-alt", + "attributes": { + "allowMultipleOpen": { + "type": "boolean", + "default": false + }, + "iconStyle": { + "type": "string", + "default": "chevron", + "enum": [ + "chevron", + "plus-minus", + "caret", + "none" + ] + }, + "iconPosition": { + "type": "string", + "default": "right", + "enum": [ + "left", + "right" + ] + }, + "borderBetween": { + "type": "boolean", + "default": true + }, + "borderBetweenColor": { + "type": "string", + "default": "" + }, + "itemGap": { + "type": "string", + "default": "0.5rem" + }, + "openBackgroundColor": { + "type": "string", + "default": "" + }, + "openTextColor": { + "type": "string", + "default": "" + }, + "hoverBackgroundColor": { + "type": "string", + "default": "" + }, + "hoverTextColor": { + "type": "string", + "default": "" + } + }, + "schemaMetadata": { + "attributeDescriptions": { + "allowMultipleOpen": "Allow multiple accordion items to be expanded simultaneously", + "iconStyle": "Visual style for the expand/collapse indicator icon", + "iconPosition": "Position of the expand/collapse icon relative to the title", + "borderBetween": "Show a horizontal border between accordion items", + "borderBetweenColor": "Color of the border between accordion items", + "itemGap": "Vertical spacing between accordion items (CSS value)", + "openBackgroundColor": "Background color for expanded accordion items", + "openTextColor": "Text color for expanded accordion items", + "hoverBackgroundColor": "Background color when hovering over an accordion item", + "hoverTextColor": "Text color when hovering over an accordion item" + }, + "htmlMappings": { + "dataAttributes": { + "allowMultipleOpen": "allow-multiple", + "iconStyle": "icon-style" + }, + "cssModifiers": { + "allowMultipleOpen": { + "true": "airo-wp-accordion--multiple", + "false": "" + }, + "iconPosition": "airo-wp-accordion--icon-{value}", + "borderBetween": { + "true": "airo-wp-accordion--border-between", + "false": "" + } + }, + "cssVariables": { + "itemGap": "--airo-wp-accordion-gap", + "openBackgroundColor": "--airo-wp-accordion-open-bg", + "openTextColor": "--airo-wp-accordion-open-text", + "hoverBackgroundColor": "--airo-wp-accordion-hover-bg", + "hoverTextColor": "--airo-wp-accordion-hover-text" + } + } + }, + "providesContext": { + "airo-wp/accordion/allowMultipleOpen": "allowMultipleOpen", + "airo-wp/accordion/iconStyle": "iconStyle", + "airo-wp/accordion/iconPosition": "iconPosition", + "airo-wp/accordion/borderBetweenColor": "borderBetweenColor", + "airo-wp/accordion/openBackgroundColor": "openBackgroundColor", + "airo-wp/accordion/openTextColor": "openTextColor", + "airo-wp/accordion/hoverBackgroundColor": "hoverBackgroundColor", + "airo-wp/accordion/hoverTextColor": "hoverTextColor" + }, + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "inserter": true, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "color": { + "background": true, + "text": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true, + "lineHeight": true, + "fontWeight": true + }, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + } + }, + "example": { + "attributes": { + "allowMultipleOpen": false + }, + "innerBlocks": [ + { + "name": "airo-wp/accordion-item", + "attributes": { + "title": "What is an accordion?", + "isOpen": true + }, + "innerBlocks": [ + { + "name": "core/paragraph", + "attributes": { + "content": "An accordion is a vertically stacked list of items that can be expanded or collapsed to show or hide content." + } + } + ] + }, + { + "name": "airo-wp/accordion-item", + "attributes": { + "title": "How do I use this block?", + "isOpen": false + }, + "innerBlocks": [ + { + "name": "core/paragraph", + "attributes": { + "content": "Add accordion items and customize the title. You can add any content inside each item using the block editor." + } + } + ] + } + ] + }, + "viewScript": "file:./view.js", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/accordion/components/AccordionPlaceholder.js b/src/blocks/accordion/components/AccordionPlaceholder.js new file mode 100644 index 0000000..de42e5c --- /dev/null +++ b/src/blocks/accordion/components/AccordionPlaceholder.js @@ -0,0 +1,27 @@ +/** + * Accordion Placeholder + * + * Thin wrapper around the shared DsgoBlockPlaceholder primitive that supplies + * the accordion-specific copy and starter templates. + */ + +import { __ } from '@wordpress/i18n'; +import DsgoBlockPlaceholder from '../../../components/shared/DsgoBlockPlaceholder'; +import accordionTemplates from '../templates'; + +export default function AccordionPlaceholder({ clientId, setAttributes }) { + return ( + + ); +} diff --git a/src/blocks/accordion/deprecated.js b/src/blocks/accordion/deprecated.js new file mode 100644 index 0000000..a2f4d9a --- /dev/null +++ b/src/blocks/accordion/deprecated.js @@ -0,0 +1,169 @@ +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +// v1: save() emitted empty --airo-wp-accordion-* custom properties even when the +// colors were unset; current save() omits them. Markup-only change → passthrough +// migrate. isEligible matches the legacy empty-declaration signature. +const v1 = { + attributes: { + allowMultipleOpen: { + type: 'boolean', + default: false, + }, + iconStyle: { + type: 'string', + default: 'chevron', + enum: ['chevron', 'plus-minus', 'caret', 'none'], + }, + iconPosition: { + type: 'string', + default: 'right', + enum: ['left', 'right'], + }, + borderBetween: { + type: 'boolean', + default: true, + }, + borderBetweenColor: { + type: 'string', + default: '', + }, + itemGap: { + type: 'string', + default: '0.5rem', + }, + openBackgroundColor: { + type: 'string', + default: '', + }, + openTextColor: { + type: 'string', + default: '', + }, + hoverBackgroundColor: { + type: 'string', + default: '', + }, + hoverTextColor: { + type: 'string', + default: '', + }, + }, + supports: { + anchor: true, + align: ['wide', 'full'], + html: false, + inserter: true, + spacing: { + margin: true, + padding: true, + blockGap: true, + __experimentalDefaultControls: { + padding: true, + blockGap: true, + }, + }, + color: { + background: true, + text: true, + link: true, + __experimentalDefaultControls: { + background: true, + text: true, + }, + }, + typography: { + fontSize: true, + lineHeight: true, + __experimentalDefaultControls: { + fontSize: true, + lineHeight: true, + fontWeight: true, + }, + __experimentalFontFamily: true, + __experimentalFontWeight: true, + }, + __experimentalBorder: { + color: true, + radius: true, + style: true, + width: true, + __experimentalDefaultControls: { + color: true, + radius: true, + style: true, + width: true, + }, + }, + }, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + return !!innerHTML && innerHTML.includes('--airo-wp-accordion-open-bg:;'); + }, + save({ attributes }) { + const { + allowMultipleOpen, + iconStyle, + iconPosition, + borderBetween, + borderBetweenColor, + itemGap, + openBackgroundColor, + openTextColor, + hoverBackgroundColor, + hoverTextColor, + } = attributes; + + const effectiveHoverBg = hoverBackgroundColor || openBackgroundColor; + const effectiveHoverText = hoverTextColor || openTextColor; + + const accordionClasses = classnames('airo-wp-accordion', { + 'airo-wp-accordion--multiple': allowMultipleOpen, + 'airo-wp-accordion--icon-left': iconPosition === 'left', + 'airo-wp-accordion--icon-right': iconPosition === 'right', + 'airo-wp-accordion--no-icon': iconStyle === 'none', + 'airo-wp-accordion--border-between': borderBetween, + }); + + // OLD behavior: empty string fallback so the declaration is always emitted. + const customStyles = { + '--airo-wp-accordion-open-bg': + convertColorToCSSVar(openBackgroundColor) || '', + '--airo-wp-accordion-open-text': + convertColorToCSSVar(openTextColor) || '', + '--airo-wp-accordion-hover-bg': + convertColorToCSSVar(effectiveHoverBg) || '', + '--airo-wp-accordion-hover-text': + convertColorToCSSVar(effectiveHoverText) || '', + '--airo-wp-accordion-gap': itemGap, + ...(borderBetweenColor && { + '--airo-wp-accordion-border-color': + convertColorToCSSVar(borderBetweenColor), + }), + }; + + const blockProps = useBlockProps.save({ + className: accordionClasses, + style: customStyles, + 'data-allow-multiple': allowMultipleOpen, + 'data-icon-style': iconStyle, + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-accordion__items', + }); + + return ( +
    +
    +
    + ); + }, + migrate(attributes) { + return attributes; + }, +}; + +export default [v1]; diff --git a/src/blocks/accordion/edit.js b/src/blocks/accordion/edit.js new file mode 100644 index 0000000..13f8306 --- /dev/null +++ b/src/blocks/accordion/edit.js @@ -0,0 +1,423 @@ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + store as blockEditorStore, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { + ToggleControl, + SelectControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useSelect } from '@wordpress/data'; +import classnames from 'classnames'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import AccordionPlaceholder from './components/AccordionPlaceholder'; + +export default function AccordionEdit({ attributes, setAttributes, clientId }) { + const { + allowMultipleOpen, + iconStyle, + iconPosition, + borderBetween, + borderBetweenColor, + itemGap, + openBackgroundColor, + openTextColor, + hoverBackgroundColor, + hoverTextColor, + } = attributes; + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + const hasInnerBlocks = useSelect( + (select) => + select(blockEditorStore).getBlock(clientId)?.innerBlocks?.length > + 0, + [clientId] + ); + + // Smart default: hover mirrors open unless explicitly set + const effectiveHoverBg = hoverBackgroundColor || openBackgroundColor; + const effectiveHoverText = hoverTextColor || openTextColor; + + // Declaratively calculate classes based on attributes + const accordionClasses = classnames('airo-wp-accordion', { + 'airo-wp-accordion--multiple': allowMultipleOpen, + 'airo-wp-accordion--icon-left': iconPosition === 'left', + 'airo-wp-accordion--icon-right': iconPosition === 'right', + 'airo-wp-accordion--no-icon': iconStyle === 'none', + 'airo-wp-accordion--border-between': borderBetween, + }); + + // Apply colors and gap as CSS custom properties that will cascade to accordion items + const customStyles = { + '--airo-wp-accordion-open-bg': convertColorToCSSVar(openBackgroundColor), + '--airo-wp-accordion-open-text': convertColorToCSSVar(openTextColor), + '--airo-wp-accordion-hover-bg': convertColorToCSSVar(effectiveHoverBg), + '--airo-wp-accordion-hover-text': convertColorToCSSVar(effectiveHoverText), + '--airo-wp-accordion-gap': itemGap, + ...(borderBetweenColor && { + '--airo-wp-accordion-border-color': + convertColorToCSSVar(borderBetweenColor), + }), + }; + + // Block wrapper props + const blockProps = useBlockProps({ + className: accordionClasses, + style: customStyles, + }); + + // Inner blocks configuration - ONLY allow accordion-item children. The + // initial seeding is handled by AccordionPlaceholder so the user always + // picks a starter layout instead of landing on a generic two-item template. + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-accordion__items', + }, + { + allowedBlocks: ['airo-wp/accordion-item'], + orientation: 'vertical', + } + ); + + if (!hasInnerBlocks) { + return ( +
    + +
    + ); + } + + return ( + <> + + + setAttributes({ + allowMultipleOpen: false, + iconStyle: 'chevron', + iconPosition: 'right', + borderBetween: true, + itemGap: '0.5rem', + }) + } + > + allowMultipleOpen !== false} + onDeselect={() => + setAttributes({ allowMultipleOpen: false }) + } + isShownByDefault + > + + setAttributes({ allowMultipleOpen: value }) + } + __nextHasNoMarginBottom + /> + + + iconStyle !== 'chevron'} + onDeselect={() => + setAttributes({ iconStyle: 'chevron' }) + } + isShownByDefault + > + + setAttributes({ iconStyle: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {iconStyle !== 'none' && ( + iconPosition !== 'right'} + onDeselect={() => + setAttributes({ iconPosition: 'right' }) + } + isShownByDefault + > + + setAttributes({ iconPosition: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + borderBetween !== true} + onDeselect={() => + setAttributes({ borderBetween: true }) + } + isShownByDefault + > + + setAttributes({ borderBetween: value }) + } + help={ + borderBetween + ? __( + 'Items have borders between them with no gap', + 'airo-wp' + ) + : __( + 'Items are separated with spacing', + 'airo-wp' + ) + } + __nextHasNoMarginBottom + /> + + + {!borderBetween && ( + itemGap !== '0.5rem'} + onDeselect={() => + setAttributes({ itemGap: '0.5rem' }) + } + isShownByDefault + > + + setAttributes({ + itemGap: value || '0.5rem', + }) + } + units={[ + { value: 'px', label: 'px', default: 8 }, + { + value: 'rem', + label: 'rem', + default: 0.5, + }, + { value: 'em', label: 'em', default: 0.5 }, + ]} + min={0} + max={100} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + + + + {borderBetween && ( + + setAttributes({ + borderBetweenColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + clearable: true, + enableAlpha: true, + }, + ]} + {...colorGradientSettings} + /> + )} + + + setAttributes({ + openBackgroundColor: + encodeColorValue( + value, + colorGradientSettings + ) || '', + }), + clearable: true, + enableAlpha: true, + }, + { + label: __('Text', 'airo-wp'), + colorValue: decodeColorValue( + openTextColor, + colorGradientSettings + ), + onColorChange: (value) => + setAttributes({ + openTextColor: + encodeColorValue( + value, + colorGradientSettings + ) || '', + }), + clearable: true, + enableAlpha: true, + }, + ]} + {...colorGradientSettings} + /> +

    + {__( + 'Colors applied to all accordion items when open.', + 'airo-wp' + )} +

    + + + setAttributes({ + hoverBackgroundColor: + encodeColorValue( + value, + colorGradientSettings + ) || '', + }), + clearable: true, + enableAlpha: true, + }, + { + label: __('Text', 'airo-wp'), + colorValue: decodeColorValue( + hoverTextColor, + colorGradientSettings + ), + onColorChange: (value) => + setAttributes({ + hoverTextColor: + encodeColorValue( + value, + colorGradientSettings + ) || '', + }), + clearable: true, + enableAlpha: true, + }, + ]} + {...colorGradientSettings} + /> +

    + {hoverBackgroundColor || hoverTextColor + ? __( + 'Custom hover colors set. Clear to use open state colors.', + 'airo-wp' + ) + : __( + 'Hover colors mirror open state by default. Set custom colors to override.', + 'airo-wp' + )} +

    +
    + + {/* NO wrapper div - spread props directly per WordPress best practices */} +
    +
    +
    + + ); +} diff --git a/src/blocks/accordion/editor.scss b/src/blocks/accordion/editor.scss new file mode 100644 index 0000000..c9fa5ea --- /dev/null +++ b/src/blocks/accordion/editor.scss @@ -0,0 +1,86 @@ +/** + * Accordion Block - Editor Styles + * Import frontend styles + editor-specific overrides + */ + +// Import all frontend styles so they appear in editor +@use './style.scss' as *; + +// Editor-specific overrides and enhancements +.editor-styles-wrapper, +.block-editor-block-list__layout { + + .airo-wp-accordion { + // Editor visual aids for container + position: relative; + width: 100%; // CRITICAL: Must be duplicated from style.scss for editor/frontend parity + + // Add subtle border in editor to show container bounds + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px dashed rgba(0, 124, 186, 0.2); + pointer-events: none; + border-radius: var(--airo-wp-accordion-border-radius); + } + + // When selected, show more prominent border + &.is-selected { + + &::before { + border-color: var(--wp--admin-theme-color, #007cba); + border-style: solid; + border-width: 2px; + } + } + + // Visual aids for settings + &.airo-wp-accordion--border-between { + // Already has borders, less prominent container border + &::before { + opacity: 0.5; + } + } + + &.airo-wp-accordion--multiple { + // Visual indicator for multiple mode + &::after { + content: 'Multiple panels can open'; + position: absolute; + top: -1.5rem; + left: 0; + font-size: 0.75rem; + color: var(--wp--preset--color--primary, #007cba); + background: rgba(0, 124, 186, 0.1); + padding: 0.25rem 0.5rem; + border-radius: 4px; + pointer-events: none; + } + } + + // Better spacing for inner blocks in editor + .airo-wp-accordion__items { + + > .block-editor-inner-blocks > .block-editor-block-list__layout { + // Remove default WordPress inner blocks spacing + padding: 0; + } + } + } +} + +// Block preview (inserter/pattern library) +.block-editor-block-preview__content { + + .airo-wp-accordion { + // Remove visual aids in preview + &::before, + &::after { + display: none; + } + } +} diff --git a/src/blocks/accordion/index.js b/src/blocks/accordion/index.js new file mode 100644 index 0000000..8cc910b --- /dev/null +++ b/src/blocks/accordion/index.js @@ -0,0 +1,36 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +// Import styles +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + deprecated, + icon: { + src: ( + + + + + + + ), + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, +}); diff --git a/src/blocks/accordion/save.js b/src/blocks/accordion/save.js new file mode 100644 index 0000000..5fd3185 --- /dev/null +++ b/src/blocks/accordion/save.js @@ -0,0 +1,62 @@ +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +export default function AccordionSave({ attributes }) { + const { + allowMultipleOpen, + iconStyle, + iconPosition, + borderBetween, + borderBetweenColor, + itemGap, + openBackgroundColor, + openTextColor, + hoverBackgroundColor, + hoverTextColor, + } = attributes; + + // Smart default: hover mirrors open unless explicitly set + const effectiveHoverBg = hoverBackgroundColor || openBackgroundColor; + const effectiveHoverText = hoverTextColor || openTextColor; + + // Same classes as edit.js - MUST MATCH EXACTLY + const accordionClasses = classnames('airo-wp-accordion', { + 'airo-wp-accordion--multiple': allowMultipleOpen, + 'airo-wp-accordion--icon-left': iconPosition === 'left', + 'airo-wp-accordion--icon-right': iconPosition === 'right', + 'airo-wp-accordion--no-icon': iconStyle === 'none', + 'airo-wp-accordion--border-between': borderBetween, + }); + + // Apply colors and gap as CSS custom properties that will cascade to accordion items + const customStyles = { + '--airo-wp-accordion-open-bg': convertColorToCSSVar(openBackgroundColor), + '--airo-wp-accordion-open-text': convertColorToCSSVar(openTextColor), + '--airo-wp-accordion-hover-bg': convertColorToCSSVar(effectiveHoverBg), + '--airo-wp-accordion-hover-text': convertColorToCSSVar(effectiveHoverText), + '--airo-wp-accordion-gap': itemGap, + ...(borderBetweenColor && { + '--airo-wp-accordion-border-color': + convertColorToCSSVar(borderBetweenColor), + }), + }; + + // Use .save() variant for save function + const blockProps = useBlockProps.save({ + className: accordionClasses, + style: customStyles, + 'data-allow-multiple': allowMultipleOpen, + 'data-icon-style': iconStyle, + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-accordion__items', + }); + + return ( +
    +
    +
    + ); +} diff --git a/src/blocks/accordion/style.scss b/src/blocks/accordion/style.scss new file mode 100644 index 0000000..a4baa95 --- /dev/null +++ b/src/blocks/accordion/style.scss @@ -0,0 +1,139 @@ +/** + * Accordion Block - Frontend Styles + * Professional, polished accordion with consistent airo-wp styling + */ + +.airo-wp-accordion { + // CSS Custom Properties for easy theming + --airo-wp-accordion-gap: var(--wp--preset--spacing--20, 0.5rem); + --airo-wp-accordion-border-color: var(--wp--preset--color--contrast, rgba(0, 0, 0, 0.1)); + --airo-wp-accordion-border-radius: 8px; + --airo-wp-accordion-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + // Reset any default styles + list-style: none; + margin: 0; + padding: 0; + position: relative; + width: 100%; // Fill container width + + // Optional: subtle border around entire accordion + border-radius: var(--airo-wp-accordion-border-radius); + overflow: hidden; + + /** + * Skip Link (Accessibility) + * Visually hidden until focused - allows keyboard users to skip accordion + */ + .airo-wp-accordion__skip-link { + position: absolute; + left: -9999px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; + + &:focus { + position: relative; + left: auto; + width: auto; + height: auto; + padding: 0.5rem 1rem; + margin-bottom: 0.5rem; + background: var(--wp--preset--color--accent-2, #2563eb); + color: var(--wp--preset--color--base, #fff); + text-decoration: none; + border-radius: 4px; + font-weight: 600; + display: inline-block; + } + } + + // Container for accordion items + .airo-wp-accordion__items { + display: flex; + flex-direction: column; + gap: var(--airo-wp-accordion-gap); + } + + // Border between items - refined styling + &.airo-wp-accordion--border-between { + + .airo-wp-accordion__items { + gap: 0; + } + + .airo-wp-accordion-item:not(:last-child) { + border-bottom: 1px solid var(--airo-wp-accordion-border-color); + } + + // First item rounded top + .airo-wp-accordion-item:first-child { + border-top-left-radius: var(--airo-wp-accordion-border-radius); + border-top-right-radius: var(--airo-wp-accordion-border-radius); + overflow: hidden; + } + + // Last item rounded bottom + .airo-wp-accordion-item:last-child { + border-bottom-left-radius: var(--airo-wp-accordion-border-radius); + border-bottom-right-radius: var(--airo-wp-accordion-border-radius); + overflow: hidden; + } + } + + // Icon positioning variants + &.airo-wp-accordion--icon-left { + // Custom styles if needed + } + + &.airo-wp-accordion--icon-right { + // Custom styles if needed + } + + &.airo-wp-accordion--no-icon { + + .airo-wp-accordion-item__icon { + display: none; + } + } + + // Multiple open mode indicator + &.airo-wp-accordion--multiple { + // Optional: visual indicator for multiple mode + } + + // Full width alignment support + &.alignwide, + &.alignfull { + width: 100%; + } + + // Card style variant (optional enhancement) + &.is-style-card { + border: 1px solid var(--airo-wp-accordion-border-color); + box-shadow: var(--airo-wp-accordion-shadow); + border-radius: var(--airo-wp-accordion-border-radius); + padding: var(--wp--preset--spacing--20, 0.5rem); + background: var(--wp--preset--color--base, #fff); + } + + // Minimal style variant + &.is-style-minimal { + --airo-wp-accordion-border-color: transparent; + --airo-wp-accordion-shadow: none; + + .airo-wp-accordion-item { + background-color: transparent; + } + } +} + +// Dark mode support +@media (prefers-color-scheme: dark) { + + .airo-wp-accordion { + --airo-wp-accordion-border-color: rgba(255, 255, 255, 0.1); + --airo-wp-accordion-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + } +} diff --git a/src/blocks/accordion/templates.js b/src/blocks/accordion/templates.js new file mode 100644 index 0000000..ed9db6c --- /dev/null +++ b/src/blocks/accordion/templates.js @@ -0,0 +1,226 @@ +/** + * Accordion Templates + * + * Starter layouts shown by AccordionPlaceholder when the block is first + * inserted. Every template ends with at least one accordion-item so the user + * never lands in a silent-empty state after picking a tile. + */ + +import { __ } from '@wordpress/i18n'; + +const accordionTemplates = [ + { + name: 'blank', + title: __('Blank', 'airo-wp'), + description: __('Two empty items to fill in', 'airo-wp'), + icon: 'welcome-add-page', + attributes: {}, + innerBlocks: [ + [ + 'airo-wp/accordion-item', + { title: __('Accordion Item 1', 'airo-wp') }, + ], + [ + 'airo-wp/accordion-item', + { title: __('Accordion Item 2', 'airo-wp') }, + ], + ], + }, + { + name: 'faq', + title: __('FAQ', 'airo-wp'), + description: __( + 'Frequently asked questions with short answers', + 'airo-wp' + ), + icon: 'editor-help', + attributes: { + iconStyle: 'plus-minus', + iconPosition: 'right', + }, + innerBlocks: [ + [ + 'airo-wp/accordion-item', + { + title: __('What is included with my plan?', 'airo-wp'), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'Every plan includes the full block library, regular updates, and access to support. Upgrade at any time to unlock advanced layouts.', + 'airo-wp' + ), + }, + ], + ], + ], + [ + 'airo-wp/accordion-item', + { + title: __( + 'How do I cancel my subscription?', + 'airo-wp' + ), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'You can cancel anytime from your account dashboard. Your access continues until the end of the billing period.', + 'airo-wp' + ), + }, + ], + ], + ], + [ + 'airo-wp/accordion-item', + { + title: __('Do you offer refunds?', 'airo-wp'), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'We offer a 30-day money-back guarantee. Just reach out to support and we will process your refund.', + 'airo-wp' + ), + }, + ], + ], + ], + ], + }, + { + name: 'content', + title: __('Content', 'airo-wp'), + description: __( + 'Rich content sections with headings and media', + 'airo-wp' + ), + icon: 'editor-alignleft', + attributes: { + iconStyle: 'chevron', + iconPosition: 'right', + borderBetween: false, + }, + innerBlocks: [ + [ + 'airo-wp/accordion-item', + { + title: __('Overview', 'airo-wp'), + isOpen: true, + }, + [ + [ + 'core/heading', + { + level: 3, + content: __('Section heading', 'airo-wp'), + }, + ], + [ + 'core/paragraph', + { + content: __( + 'Use this section to explain a concept in depth. Add any blocks you need — paragraphs, images, columns, or buttons all work inside an accordion item.', + 'airo-wp' + ), + }, + ], + ], + ], + [ + 'airo-wp/accordion-item', + { + title: __('Details', 'airo-wp'), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'Drop in supporting details, examples, or step-by-step instructions.', + 'airo-wp' + ), + }, + ], + ], + ], + ], + }, + { + name: 'icon-list', + title: __('Icon List', 'airo-wp'), + description: __( + 'Compact list with icons and short blurbs', + 'airo-wp' + ), + icon: 'list-view', + attributes: { + iconStyle: 'caret', + iconPosition: 'left', + borderBetween: false, + itemGap: '0.25rem', + }, + innerBlocks: [ + [ + 'airo-wp/accordion-item', + { + title: __('Lightning fast', 'airo-wp'), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'Optimized assets and minimal markup keep page loads quick.', + 'airo-wp' + ), + }, + ], + ], + ], + [ + 'airo-wp/accordion-item', + { + title: __('Accessible by default', 'airo-wp'), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'ARIA roles and keyboard navigation are wired up automatically.', + 'airo-wp' + ), + }, + ], + ], + ], + [ + 'airo-wp/accordion-item', + { + title: __('Designed to extend', 'airo-wp'), + }, + [ + [ + 'core/paragraph', + { + content: __( + 'Style with theme.json or override individual items in the editor.', + 'airo-wp' + ), + }, + ], + ], + ], + ], + }, +]; + +export default accordionTemplates; diff --git a/src/blocks/accordion/view.js b/src/blocks/accordion/view.js new file mode 100644 index 0000000..05771ba --- /dev/null +++ b/src/blocks/accordion/view.js @@ -0,0 +1,265 @@ +/** + * Accordion Block - Frontend JavaScript + * Enhanced UX with smooth animations and scroll-to-view + * Following WordPress best practices - NO layout manipulation + */ + +document.addEventListener('DOMContentLoaded', function () { + initAccordions(); +}); +document.addEventListener('airo-wp-content-loaded', initAccordions); + +// Check for reduced motion preference +const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' +).matches; + +// Animation duration (matches CSS transition) +const ANIMATION_DURATION = prefersReducedMotion ? 0 : 350; + +function initAccordions() { + const accordions = document.querySelectorAll('.airo-wp-accordion'); + + accordions.forEach((accordion) => { + // Prevent duplicate initialization + if (accordion.hasAttribute('data-airo-wp-initialized')) { + return; + } + accordion.setAttribute('data-airo-wp-initialized', 'true'); + + const allowMultiple = + accordion.getAttribute('data-allow-multiple') === 'true'; + const items = accordion.querySelectorAll('.airo-wp-accordion-item'); + + // Add skip link for keyboard accessibility + const skipLink = document.createElement('a'); + skipLink.href = '#end-of-accordion'; + skipLink.className = 'airo-wp-accordion__skip-link'; + skipLink.textContent = 'Skip accordion'; + skipLink.addEventListener('click', (e) => { + e.preventDefault(); + // Focus on the element after the accordion + const nextElement = accordion.nextElementSibling; + if (nextElement && nextElement.tabIndex >= 0) { + nextElement.focus(); + } else { + // If no next focusable element, focus the last item + items[items.length - 1] + ?.querySelector('.airo-wp-accordion-item__trigger') + ?.focus(); + } + }); + accordion.insertBefore(skipLink, accordion.firstChild); + + items.forEach((item) => { + const trigger = item.querySelector('.airo-wp-accordion-item__trigger'); + const panel = item.querySelector('.airo-wp-accordion-item__panel'); + + if (!trigger || !panel) { + return; + } + + // Set initial state based on data attribute + const initiallyOpen = + item.getAttribute('data-initially-open') === 'true'; + if (initiallyOpen) { + openPanel(item, panel, false); // No animation on initial load + } else { + closePanel(item, panel, false); // No animation on initial load + } + + // Click handler + trigger.addEventListener('click', (e) => { + e.preventDefault(); + const isOpen = item.classList.contains( + 'airo-wp-accordion-item--open' + ); + + if (isOpen) { + // Close this panel + closePanel(item, panel, true); + } else { + // If single mode, close all other panels first + if (!allowMultiple) { + closeAllPanels(accordion); + } + // Open this panel with smooth scroll + openPanel(item, panel, true, true); + } + }); + + // Keyboard accessibility - Arrow key navigation + trigger.addEventListener('keydown', (e) => { + // Enter or Space - toggle + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + trigger.click(); + } + + // Arrow Up - focus previous item + if (e.key === 'ArrowUp') { + e.preventDefault(); + const prevItem = getPreviousItem(item, items); + if (prevItem) { + prevItem + .querySelector('.airo-wp-accordion-item__trigger') + ?.focus(); + } + } + + // Arrow Down - focus next item + if (e.key === 'ArrowDown') { + e.preventDefault(); + const nextItem = getNextItem(item, items); + if (nextItem) { + nextItem + .querySelector('.airo-wp-accordion-item__trigger') + ?.focus(); + } + } + + // Home - focus first item + if (e.key === 'Home') { + e.preventDefault(); + items[0] + ?.querySelector('.airo-wp-accordion-item__trigger') + ?.focus(); + } + + // End - focus last item + if (e.key === 'End') { + e.preventDefault(); + items[items.length - 1] + ?.querySelector('.airo-wp-accordion-item__trigger') + ?.focus(); + } + }); + }); + }); +} + +function openPanel(item, panel, animate = true, scrollIntoView = false) { + // Update classes + item.classList.remove('airo-wp-accordion-item--closed'); + item.classList.add('airo-wp-accordion-item--open'); + + // Update ARIA + const trigger = item.querySelector('.airo-wp-accordion-item__trigger'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'true'); + } + + // Show panel + panel.hidden = false; + + // Get the natural height + const content = panel.querySelector('.airo-wp-accordion-item__content'); + const contentHeight = content ? content.scrollHeight : panel.scrollHeight; + + if (!animate || prefersReducedMotion) { + // No animation - set height immediately + panel.style.height = ''; + return; + } + + // Animate height + panel.style.height = '0px'; + // eslint-disable-next-line no-undef + requestAnimationFrame(() => { + panel.style.height = `${contentHeight}px`; + + // Smooth scroll into view after opening + if (scrollIntoView) { + setTimeout(() => { + scrollItemIntoView(item); + }, ANIMATION_DURATION / 2); // Scroll halfway through animation + } + + // Remove inline height after animation completes + setTimeout(() => { + panel.style.height = ''; + }, ANIMATION_DURATION); + }); +} + +function closePanel(item, panel, animate = true) { + // Get current height for animation + const currentHeight = panel.scrollHeight; + + // Update classes + item.classList.remove('airo-wp-accordion-item--open'); + item.classList.add('airo-wp-accordion-item--closed'); + + // Update ARIA + const trigger = item.querySelector('.airo-wp-accordion-item__trigger'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + + if (!animate || prefersReducedMotion) { + // No animation - hide immediately + panel.hidden = true; + panel.style.height = ''; + return; + } + + // Set height explicitly for animation + panel.style.height = `${currentHeight}px`; + + // Force reflow + // eslint-disable-next-line no-undef + requestAnimationFrame(() => { + panel.style.height = '0px'; + + // Hide after animation completes + setTimeout(() => { + panel.hidden = true; + panel.style.height = ''; + }, ANIMATION_DURATION); + }); +} + +function closeAllPanels(accordion) { + const items = accordion.querySelectorAll('.airo-wp-accordion-item'); + + items.forEach((item) => { + const panel = item.querySelector('.airo-wp-accordion-item__panel'); + if (panel && item.classList.contains('airo-wp-accordion-item--open')) { + closePanel(item, panel, true); + } + }); +} + +// Smooth scroll to show opened accordion item +function scrollItemIntoView(item) { + const itemRect = item.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const headerOffset = 100; // Account for fixed headers + + // Check if item is not fully visible + if (itemRect.top < headerOffset || itemRect.bottom > viewportHeight) { + const scrollOptions = { + behavior: prefersReducedMotion ? 'auto' : 'smooth', + block: 'nearest', + inline: 'nearest', + }; + + // Scroll with offset for better visibility + item.scrollIntoView(scrollOptions); + } +} + +// Keyboard navigation helpers +function getPreviousItem(currentItem, allItems) { + const itemsArray = Array.from(allItems); + const currentIndex = itemsArray.indexOf(currentItem); + return currentIndex > 0 ? itemsArray[currentIndex - 1] : null; +} + +function getNextItem(currentItem, allItems) { + const itemsArray = Array.from(allItems); + const currentIndex = itemsArray.indexOf(currentItem); + return currentIndex < itemsArray.length - 1 + ? itemsArray[currentIndex + 1] + : null; +} diff --git a/src/blocks/advanced-heading/block.json b/src/blocks/advanced-heading/block.json new file mode 100644 index 0000000..9fd138a --- /dev/null +++ b/src/blocks/advanced-heading/block.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/advanced-heading", + "version": "1.0.0", + "title": "Advanced Heading", + "category": "airo-wp", + "description": "Create headings with multiple font styles — mix fonts, weights, and colors within a single heading for advanced typographic designs.", + "keywords": [ + "heading", + "title", + "typography", + "multi-font", + "advanced" + ], + "textdomain": "airo-wp", + "icon": "heading", + "attributes": { + "level": { + "type": "number", + "default": 2, + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + }, + "textAlign": { + "type": "string" + } + }, + "providesContext": { + "airo-wp/advancedHeading/level": "level" + }, + "supports": { + "anchor": true, + "html": false, + "inserter": true, + "align": [ + "left", + "center", + "right", + "wide", + "full" + ], + "color": { + "text": true, + "background": true, + "gradients": true, + "__experimentalDefaultControls": { + "text": true, + "background": false + } + }, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "margin": true, + "blockGap": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true, + "lineHeight": true, + "fontFamily": true, + "fontWeight": true, + "fontStyle": true, + "textTransform": true, + "textDecoration": true, + "letterSpacing": true + }, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true + } + }, + "example": { + "attributes": { + "level": 2 + }, + "innerBlocks": [ + { + "name": "airo-wp/heading-segment", + "attributes": { + "content": "Design" + } + }, + { + "name": "airo-wp/heading-segment", + "attributes": { + "content": "SetGo" + } + } + ] + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/advanced-heading/edit.js b/src/blocks/advanced-heading/edit.js new file mode 100644 index 0000000..2fda70c --- /dev/null +++ b/src/blocks/advanced-heading/edit.js @@ -0,0 +1,135 @@ +/** + * Advanced Heading Block - Edit Component + * + * Renders a heading element containing inner blocks (heading segments) + * that each support independent typography controls. + * + * @since 2.0.0 + */ + +import classnames from 'classnames'; +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + BlockControls, + AlignmentToolbar, +} from '@wordpress/block-editor'; +import { + PanelBody, + ToolbarGroup, + ToolbarDropdownMenu, +} from '@wordpress/components'; +import { heading as headingIcon } from '@wordpress/icons'; +import { convertPresetToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const ALLOWED_BLOCKS = ['airo-wp/heading-segment']; +const TEMPLATE = [ + [ + 'airo-wp/heading-segment', + { + content: __('Bold', 'airo-wp'), + style: { typography: { fontWeight: '700' } }, + }, + ], + ['airo-wp/heading-segment', { content: __('Heading', 'airo-wp') }], +]; + +const HEADING_LEVELS = [1, 2, 3, 4, 5, 6]; + +/** + * Advanced Heading Edit Component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @return {JSX.Element} Advanced Heading block edit component + */ +export default function AdvancedHeadingEdit({ attributes, setAttributes }) { + const { level = 2, textAlign } = attributes; + const validLevel = HEADING_LEVELS.includes(level) ? level : 2; + const TagName = `h${validLevel}`; + + const blockGap = convertPresetToCSSVar(attributes.style?.spacing?.blockGap); + + const blockProps = useBlockProps({ + className: classnames('airo-wp-advanced-heading', { + [`has-text-align-${textAlign}`]: textAlign, + }), + }); + + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-advanced-heading__inner', + style: blockGap ? { '--airo-wp-segment-gap': blockGap } : undefined, + }, + { + allowedBlocks: ALLOWED_BLOCKS, + template: TEMPLATE, + orientation: 'horizontal', + } + ); + + return ( + <> + {/* ======================================== + BLOCK TOOLBAR + ======================================== */} + + + ({ + icon: headingIcon, + title: `H${targetLevel}`, + isActive: level === targetLevel, + onClick: () => + setAttributes({ level: targetLevel }), + }))} + /> + + setAttributes({ textAlign: value })} + /> + + + {/* ======================================== + INSPECTOR CONTROLS + ======================================== */} + + +

    + {__('Heading Level', 'airo-wp')} +

    +
    + {HEADING_LEVELS.map((targetLevel) => ( + + ))} +
    +
    +
    + + {/* ======================================== + BLOCK CONTENT + ======================================== */} +
    + +
    + + ); +} diff --git a/src/blocks/advanced-heading/editor.scss b/src/blocks/advanced-heading/editor.scss new file mode 100644 index 0000000..6e1df96 --- /dev/null +++ b/src/blocks/advanced-heading/editor.scss @@ -0,0 +1,72 @@ +/** + * Advanced Heading Block Editor Styles + */ + +.airo-wp-advanced-heading { + + .airo-wp-advanced-heading__inner { + margin: 0; + padding: 0; + } + + // Gap between heading segments — matches frontend style.scss + .airo-wp-heading-segment + .airo-wp-heading-segment { + margin-inline-start: var(--airo-wp-segment-gap, 0); + } +} + +// Heading level picker in the inspector +.airo-wp-advanced-heading__level-label { + margin-bottom: 8px; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: var(--wp-components-color-foreground-inverted, #757575); +} + +.airo-wp-advanced-heading__level-buttons { + display: flex; + gap: 4px; + margin-bottom: 16px; +} + +.airo-wp-advanced-heading__level-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 36px; + height: 36px; + padding: 0 8px; + border: 1px solid var(--wp-components-color-accent-inverted, #ddd); + border-radius: 4px; + background: var(--wp-components-color-background, #fff); + color: var(--wp-components-color-foreground, #1e1e1e); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.12s ease; + + &:hover { + background: var(--wp-components-color-gray-100, #f0f0f0); + border-color: var(--wp-components-color-gray-400, #999); + } + + &.is-active { + background: var(--wp-components-color-foreground, #1e1e1e); + border-color: var(--wp-components-color-foreground, #1e1e1e); + color: var(--wp-components-color-background, #fff); + } +} + +// Text alignment support in editor +.airo-wp-advanced-heading.has-text-align-left .airo-wp-advanced-heading__inner { + text-align: left; +} + +.airo-wp-advanced-heading.has-text-align-center .airo-wp-advanced-heading__inner { + text-align: center; +} + +.airo-wp-advanced-heading.has-text-align-right .airo-wp-advanced-heading__inner { + text-align: right; +} diff --git a/src/blocks/advanced-heading/index.js b/src/blocks/advanced-heading/index.js new file mode 100644 index 0000000..a817821 --- /dev/null +++ b/src/blocks/advanced-heading/index.js @@ -0,0 +1,46 @@ +/** + * Advanced Heading Block - Registration + * + * A heading block that supports multiple font segments, + * allowing different typography for each part of the heading. + * + * @since 2.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +/** + * Register Advanced Heading Block + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, +}); diff --git a/src/blocks/advanced-heading/save.js b/src/blocks/advanced-heading/save.js new file mode 100644 index 0000000..2d0d878 --- /dev/null +++ b/src/blocks/advanced-heading/save.js @@ -0,0 +1,46 @@ +/** + * Advanced Heading Block - Save Function + * + * Renders the heading element with inner block content. + * Each heading segment saves its own typography styles. + * + * @since 2.0.0 + */ + +import classnames from 'classnames'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { convertPresetToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const HEADING_LEVELS = [1, 2, 3, 4, 5, 6]; + +/** + * Advanced Heading Save Function + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @return {JSX.Element} Saved advanced heading block markup + */ +export default function AdvancedHeadingSave({ attributes }) { + const { level = 2, textAlign } = attributes; + const validLevel = HEADING_LEVELS.includes(level) ? level : 2; + const TagName = `h${validLevel}`; + + const blockGap = convertPresetToCSSVar(attributes.style?.spacing?.blockGap); + + const blockProps = useBlockProps.save({ + className: classnames('airo-wp-advanced-heading', { + [`has-text-align-${textAlign}`]: textAlign, + }), + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-advanced-heading__inner', + style: blockGap ? { '--airo-wp-segment-gap': blockGap } : undefined, + }); + + return ( +
    + +
    + ); +} diff --git a/src/blocks/advanced-heading/style.scss b/src/blocks/advanced-heading/style.scss new file mode 100644 index 0000000..1968235 --- /dev/null +++ b/src/blocks/advanced-heading/style.scss @@ -0,0 +1,31 @@ +/** + * Advanced Heading Block Frontend Styles + */ + +.airo-wp-advanced-heading { + // Reset margin on the heading element itself — spacing + // is handled by WordPress Block Supports on the wrapper + .airo-wp-advanced-heading__inner { + margin: 0; + padding: 0; + } + + // Gap between heading segments — defaults to 0 so segments run + // together as one heading; editor block-gap control can override + .airo-wp-heading-segment + .airo-wp-heading-segment { + margin-inline-start: var(--airo-wp-segment-gap, 0); + } +} + +// Text alignment support +.airo-wp-advanced-heading.has-text-align-left .airo-wp-advanced-heading__inner { + text-align: left; +} + +.airo-wp-advanced-heading.has-text-align-center .airo-wp-advanced-heading__inner { + text-align: center; +} + +.airo-wp-advanced-heading.has-text-align-right .airo-wp-advanced-heading__inner { + text-align: right; +} diff --git a/src/blocks/blobs/block.json b/src/blocks/blobs/block.json new file mode 100644 index 0000000..16eb979 --- /dev/null +++ b/src/blocks/blobs/block.json @@ -0,0 +1,166 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/blobs", + "version": "1.0.0", + "title": "Blobs", + "category": "airo-wp", + "description": "Create random, unique, and organic-looking blob shapes. Customize with gradients, overlays, and images. Animate with smooth morphing effects.", + "keywords": [ + "blob", + "shape", + "organic", + "morph", + "animate", + "gradient" + ], + "textdomain": "airo-wp", + "icon": "art", + "attributes": { + "align": { + "type": "string" + }, + "blobShape": { + "type": "string", + "default": "shape-1", + "enum": [ + "shape-1", + "shape-2", + "shape-3", + "shape-4", + "shape-5", + "shape-6" + ] + }, + "blobAnimation": { + "type": "string", + "default": "none", + "enum": [ + "morph-1", + "morph-2", + "float", + "pulse", + "spin", + "none" + ] + }, + "animationDuration": { + "type": "string", + "default": "8s" + }, + "animationEasing": { + "type": "string", + "default": "ease-in-out", + "enum": [ + "linear", + "ease", + "ease-in", + "ease-out", + "ease-in-out" + ] + }, + "size": { + "type": "string", + "default": "300px" + }, + "height": { + "type": "string", + "default": "" + }, + "maxWidth": { + "type": "string" + }, + "enableOverlay": { + "type": "boolean", + "default": false + }, + "overlayColor": { + "type": "string", + "default": "" + }, + "overlayOpacity": { + "type": "number", + "default": 80 + } + }, + "supports": { + "anchor": true, + "align": [ + "left", + "center", + "right", + "wide", + "full" + ], + "html": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": true, + "padding": true + } + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "background": { + "backgroundImage": true, + "backgroundSize": true, + "backgroundPosition": true, + "__experimentalDefaultControls": { + "backgroundImage": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + }, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalWritingMode": true + }, + "__experimentalBorder": { + "radius": false, + "__experimentalDefaultControls": { + "radius": false + } + } + }, + "example": { + "attributes": { + "blobAnimation": "morph-1", + "size": "300px", + "backgroundColor": "accent-2" + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "level": 2, + "content": "Blob Shape", + "textAlign": "center" + } + }, + { + "name": "core/paragraph", + "attributes": { + "content": "Organic animated shapes", + "align": "center" + } + } + ] + }, + "editorScript": "file:./index.js", + "viewScript": "file:./view.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/blobs/deprecated.js b/src/blocks/blobs/deprecated.js new file mode 100644 index 0000000..c07460a --- /dev/null +++ b/src/blocks/blobs/deprecated.js @@ -0,0 +1,457 @@ +/** + * Blobs Block - Deprecated Versions + * + * Handles backwards compatibility for old blob block markup + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { + convertPresetToCSSVar, + convertColorToCSSVar, +} from '../../utils/convert-preset-to-css-var'; +import { getOwnOpeningTag } from '../../utils/get-own-opening-tag'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Shared supports for all deprecated versions. + * Uses __experimentalBorder (the historical name) instead of border. + */ +const sharedSupports = { + anchor: true, + align: ['left', 'center', 'right', 'wide', 'full'], + html: false, + spacing: { + margin: true, + padding: true, + __experimentalDefaultControls: { + margin: true, + padding: true, + }, + }, + color: { + background: true, + text: true, + gradients: true, + __experimentalDefaultControls: { + background: true, + text: true, + }, + }, + background: { + backgroundImage: true, + backgroundSize: true, + backgroundPosition: true, + __experimentalDefaultControls: { + backgroundImage: true, + }, + }, + typography: { + fontSize: true, + lineHeight: true, + writingMode: true, + fontFamily: true, + fontWeight: true, + __experimentalDefaultControls: { + fontSize: true, + }, + }, + __experimentalBorder: { + radius: false, + __experimentalDefaultControls: { + radius: false, + }, + }, +}; + +/** + * Version 3: Before the native, kit-controllable max-width control + * + * Blobs used to receive its max-width from the generic `max-width` extension + * (src/extensions/max-width), which stored a `dsgoMaxWidth` attribute and, on + * save, added the `airo-wp-has-max-width` class plus an inline + * `max-width;margin-left:auto;margin-right:auto` to the wrapper. Blobs now owns + * a native `maxWidth` attribute that emits the class + a kit-controllable + * `--airo-wp-blob-max-width` custom property instead (the raw max-width / centering + * margins moved to the stylesheet), and is excluded from the generic extension. + * + * This deprecation detects the old extension markup (wrapper carries + * `airo-wp-has-max-width` with a RAW inline `max-width:` — distinct from the new + * `--airo-wp-blob-max-width:` var) and migrates `dsgoMaxWidth` → `maxWidth` so + * existing content upgrades silently instead of showing a recovery warning. + * Blobs without a max-width are unaffected (they never carried the class) and + * validate directly against the current save(). + */ +const v3 = { + supports: sharedSupports, + attributes: { + align: { + type: 'string', + }, + blobShape: { + type: 'string', + default: 'shape-1', + }, + blobAnimation: { + type: 'string', + default: 'none', + }, + animationDuration: { + type: 'string', + default: '8s', + }, + animationEasing: { + type: 'string', + default: 'ease-in-out', + }, + size: { + type: 'string', + default: '300px', + }, + height: { + type: 'string', + default: '', + }, + enableOverlay: { + type: 'boolean', + default: false, + }, + overlayColor: { + type: 'string', + default: '', + }, + overlayOpacity: { + type: 'number', + default: 80, + }, + // Legacy attribute injected by the generic max-width extension. + dsgoMaxWidth: { + type: 'string', + default: '', + }, + }, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Scope the check to the Blobs wrapper's OWN opening tag. Blobs accepts + // arbitrary nested blocks, and the generic max-width extension still + // stamps the same `airo-wp-has-max-width` class + a raw inline `max-width:` + // onto any non-excluded nested child — so scanning the whole subtree + // could false-match a valid *native* Blobs block that merely contains + // such a child. Old extension markup put the raw max-width on the wrapper + // root; the new native format writes `--airo-wp-blob-max-width` there + // instead (excluded by the `[^-]` guard). + const openingTag = getOwnOpeningTag(innerHTML, 'airo-wp-blobs-wrapper'); + if (!openingTag) { + return false; + } + return ( + openingTag.includes('airo-wp-has-max-width') && + /(?:^|[^-])max-width:/.test(openingTag) + ); + }, + save({ attributes }) { + const { + blobShape, + blobAnimation, + animationDuration, + animationEasing, + size, + height, + enableOverlay, + overlayColor, + overlayOpacity, + dsgoMaxWidth, + align, + } = attributes; + + const blobClasses = classnames('airo-wp-blobs', { + [`airo-wp-blobs--${blobShape}`]: blobShape, + [`airo-wp-blobs--${blobAnimation}`]: + blobAnimation && blobAnimation !== 'none', + }); + + const customStyles = { + '--airo-wp-blob-size': size, + ...(height ? { '--airo-wp-blob-height': height } : {}), + '--airo-wp-blob-animation-duration': animationDuration, + '--airo-wp-blob-animation-easing': animationEasing, + }; + + // Reproduce the generic extension's align-aware centering margins. + let marginLeft = 'auto'; + let marginRight = 'auto'; + if (align === 'left') { + marginLeft = '0'; + } else if (align === 'right') { + marginRight = '0'; + } + + const blockProps = useBlockProps.save({ + className: classnames('airo-wp-blobs-wrapper', { + 'airo-wp-has-max-width': !!dsgoMaxWidth, + }), + ...(dsgoMaxWidth && { + style: { + maxWidth: dsgoMaxWidth, + marginLeft, + marginRight, + }, + }), + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-blobs__content', + }); + + return ( +
    +
    + {enableOverlay && ( +
    + )} +
    +
    +
    +
    +
    + ); + }, + migrate(attributes) { + // Move the extension value onto the native attribute and drop the + // legacy key so the current save() owns the markup. + const { dsgoMaxWidth, ...rest } = attributes; + if (dsgoMaxWidth) { + return { ...rest, maxWidth: dsgoMaxWidth }; + } + return rest; + }, +}; + +// Version 1: Original structure without wrapper +const v1 = { + supports: sharedSupports, + attributes: { + blobShape: { + type: 'string', + default: 'shape-1', + }, + blobAnimation: { + type: 'string', + default: 'none', + }, + animationDuration: { + type: 'string', + default: '8s', + }, + animationEasing: { + type: 'string', + default: 'ease-in-out', + }, + size: { + type: 'string', + default: '300px', + }, + enableOverlay: { + type: 'boolean', + default: false, + }, + overlayColor: { + type: 'string', + default: '#000000', + }, + overlayOpacity: { + type: 'number', + default: 50, + }, + }, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v1 blocks have no wrapper div - the airo-wp-blobs class is directly on the block wrapper + return innerHTML && !innerHTML.includes('airo-wp-blobs-wrapper'); + }, + save: ({ attributes }) => { + const { + blobShape, + blobAnimation, + animationDuration, + animationEasing, + size, + enableOverlay, + overlayColor, + overlayOpacity, + } = attributes; + + const blobClasses = classnames('airo-wp-blobs', { + [`airo-wp-blobs--${blobShape}`]: blobShape, + [`airo-wp-blobs--${blobAnimation}`]: + blobAnimation && blobAnimation !== 'none', + }); + + const customStyles = { + '--airo-wp-blob-size': size, + '--airo-wp-blob-animation-duration': animationDuration, + '--airo-wp-blob-animation-easing': animationEasing, + }; + + const blockProps = useBlockProps.save({ + className: blobClasses, + style: customStyles, + 'data-blob-animation': blobAnimation, + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-blobs__content', + }); + + return ( +
    + {enableOverlay && ( +
    + )} +
    +
    +
    +
    + ); + }, + migrate(attributes) { + return attributes; + }, +}; + +// Version 2: With wrapper but without align attribute +const v2 = { + supports: sharedSupports, + attributes: { + blobShape: { + type: 'string', + default: 'shape-1', + }, + blobAnimation: { + type: 'string', + default: 'none', + }, + animationDuration: { + type: 'string', + default: '8s', + }, + animationEasing: { + type: 'string', + default: 'ease-in-out', + }, + size: { + type: 'string', + default: '300px', + }, + enableOverlay: { + type: 'boolean', + default: false, + }, + overlayColor: { + type: 'string', + default: '', + }, + overlayOpacity: { + type: 'number', + default: 80, + }, + }, + // No isEligible: v2's save() output is byte-identical to the current save() + // whenever `height` and `maxWidth` are unset (both are omitted from the + // markup in that case), so a v2-era block simply parses as VALID today and + // never needs migrating. It stays in the array because a v2 block that DID + // set an overlay colour still differs (convertPresetToCSSVar vs the current + // convertColorToCSSVar) and reaches this version by save-matching. + // + // The old guard was `innerHTML.includes('airo-wp-blobs-wrapper') && + // attributes.align === undefined`. The current save() still emits that + // wrapper class, and `align` has no default so it is absent from the comment + // on any block the author never aligned — i.e. the guard matched current + // content. It then migrated it through a schema that predates `height` / + // `maxWidth`, silently dropping both. + save({ attributes }) { + const { + blobShape, + blobAnimation, + animationDuration, + animationEasing, + size, + enableOverlay, + overlayColor, + overlayOpacity, + } = attributes; + + const blobClasses = classnames('airo-wp-blobs', { + [`airo-wp-blobs--${blobShape}`]: blobShape, + [`airo-wp-blobs--${blobAnimation}`]: + blobAnimation && blobAnimation !== 'none', + }); + + const customStyles = { + '--airo-wp-blob-size': size, + '--airo-wp-blob-animation-duration': animationDuration, + '--airo-wp-blob-animation-easing': animationEasing, + }; + + const blockProps = useBlockProps.save({ + className: 'airo-wp-blobs-wrapper', + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-blobs__content', + }); + + return ( +
    +
    + {enableOverlay && ( +
    + )} +
    +
    +
    +
    +
    + ); + }, + migrate(oldAttributes) { + // Migrate to new version with align attribute + return { + ...oldAttributes, + align: undefined, // WordPress will use default from supports + }; + }, +}; + +export default [v3, v2, v1]; diff --git a/src/blocks/blobs/edit.js b/src/blocks/blobs/edit.js new file mode 100644 index 0000000..59b4eb4 --- /dev/null +++ b/src/blocks/blobs/edit.js @@ -0,0 +1,589 @@ +/** + * Blobs Block - Editor Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef } from '@wordpress/element'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { + SelectControl, + RangeControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import classnames from 'classnames'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import { hasExplicitString } from '../../utils/has-explicit-value'; + +export default function BlobsEdit({ attributes, setAttributes, clientId }) { + const wrapperRef = useRef(null); + const { + blobShape, + blobAnimation, + animationDuration, + animationEasing, + size, + height, + maxWidth, + enableOverlay, + overlayColor, + overlayOpacity, + } = attributes; + + // Derive unit-specific min/max for the blob size control + const sizeUnit = size ? size.replace(/[\d.]+/, '') : 'px'; + const sizeConstraints = { + px: { min: 50, max: 800 }, + '%': { min: 20, max: 200 }, + vw: { min: 10, max: 100 }, + vh: { min: 10, max: 100 }, + }; + const { min: sizeMin, max: sizeMax } = + sizeConstraints[sizeUnit] || sizeConstraints.px; + + // Derive unit-specific min/max for the blob height control + const heightUnit = height ? height.replace(/[\d.]+/, '') : 'px'; + const { min: heightMin, max: heightMax } = + sizeConstraints[heightUnit] || sizeConstraints.px; + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Calculate classes + const blobClasses = classnames('airo-wp-blobs', { + [`airo-wp-blobs--${blobShape}`]: blobShape, + [`airo-wp-blobs--${blobAnimation}`]: + blobAnimation && blobAnimation !== 'none', + }); + + // Apply animation settings as CSS custom properties + const customStyles = { + '--airo-wp-blob-size': size, + ...(height ? { '--airo-wp-blob-height': height } : {}), + '--airo-wp-blob-animation-duration': animationDuration, + '--airo-wp-blob-animation-easing': animationEasing, + }; + + // Optional max-width constraint on the wrapper - MUST MATCH save.js. + // Only add the class + kit-controllable custom property when the author + // sets an explicit maxWidth; the stylesheet owns the actual max-width and + // centering margins. + const hasMaxWidth = hasExplicitString(maxWidth); + + // Get block props with our wrapper class + const blockProps = useBlockProps({ + className: classnames('airo-wp-blobs-wrapper', { + 'airo-wp-has-max-width': hasMaxWidth, + }), + ref: wrapperRef, + ...(hasMaxWidth && { + style: { '--airo-wp-blob-max-width': maxWidth }, + }), + }); + + // Transfer background styles from wrapper to blob in editor + useEffect(() => { + if (!wrapperRef.current) { + return; + } + + const wrapper = wrapperRef.current; + const blob = wrapper.querySelector('.airo-wp-blobs'); + if (!blob) { + return; + } + + // WordPress sets inline styles on the wrapper + // We need to read inline styles directly because our CSS has `background: none !important;` + const inlineStyle = wrapper.style; + + // Transfer background image + if ( + inlineStyle.backgroundImage && + inlineStyle.backgroundImage !== 'none' + ) { + blob.style.setProperty( + 'background-image', + inlineStyle.backgroundImage + ); + } else { + blob.style.removeProperty('background-image'); + } + + // Transfer background size + if ( + inlineStyle.backgroundSize && + inlineStyle.backgroundSize !== 'auto' + ) { + blob.style.setProperty( + 'background-size', + inlineStyle.backgroundSize + ); + } + + // Transfer background position + if (inlineStyle.backgroundPosition) { + blob.style.setProperty( + 'background-position', + inlineStyle.backgroundPosition + ); + } + + // Transfer background repeat + if ( + inlineStyle.backgroundRepeat && + inlineStyle.backgroundRepeat !== 'repeat' + ) { + blob.style.setProperty( + 'background-repeat', + inlineStyle.backgroundRepeat + ); + } + + // Transfer background attachment + if ( + inlineStyle.backgroundAttachment && + inlineStyle.backgroundAttachment !== 'scroll' + ) { + blob.style.setProperty( + 'background-attachment', + inlineStyle.backgroundAttachment + ); + } + + // Transfer WordPress background color classes from wrapper to blob + // so WordPress's own CSS applies the color to the blob shape directly + const bgClasses = Array.from(wrapper.classList).filter( + (c) => + c.match(/^has-.*-background-color$/) || c === 'has-background' + ); + bgClasses.forEach((cls) => { + wrapper.classList.remove(cls); + blob.classList.add(cls); + }); + + // Transfer inline background color (custom non-preset colors) + if (inlineStyle.backgroundColor) { + blob.style.setProperty( + 'background-color', + inlineStyle.backgroundColor + ); + } else if (bgClasses.length === 0) { + // Apply default color if no user color is set + const defaultColor = + window + .getComputedStyle(document.documentElement) + .getPropertyValue('--wp--preset--color--accent-2') || + '#2563eb'; + blob.style.setProperty('background-color', defaultColor.trim()); + } + }); // Run on every render to catch style changes + + // Inner blocks for content inside blob + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-blobs__content', + }, + { + template: [ + [ + 'core/heading', + { + level: 2, + placeholder: __('Add title…', 'airo-wp'), + textAlign: 'center', + }, + ], + [ + 'core/paragraph', + { + placeholder: __('Add description…', 'airo-wp'), + align: 'center', + }, + ], + ], + } + ); + + return ( + <> + + + setAttributes({ + blobShape: 'shape-1', + blobAnimation: 'none', + animationDuration: '8s', + animationEasing: 'ease-in-out', + size: '300px', + height: '', + maxWidth: undefined, + overlayOpacity: 80, + }) + } + > + blobShape !== 'shape-1'} + onDeselect={() => + setAttributes({ blobShape: 'shape-1' }) + } + isShownByDefault + > + + setAttributes({ blobShape: value }) + } + help={__( + 'Choose the organic shape style', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + size !== '300px'} + onDeselect={() => setAttributes({ size: '300px' })} + isShownByDefault + > + + setAttributes({ size: value || '300px' }) + } + units={[ + { value: 'px', label: 'px', default: 300 }, + { value: '%', label: '%', default: 100 }, + { value: 'vw', label: 'vw', default: 30 }, + { value: 'vh', label: 'vh', default: 30 }, + ]} + min={sizeMin} + max={sizeMax} + help={__('Width of the blob shape', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + height !== ''} + onDeselect={() => setAttributes({ height: '' })} + isShownByDefault + > + + setAttributes({ height: value }) + } + units={[ + { value: 'px', label: 'px', default: 300 }, + { value: '%', label: '%', default: 100 }, + { value: 'vw', label: 'vw', default: 30 }, + { value: 'vh', label: 'vh', default: 30 }, + ]} + min={heightMin} + max={heightMax} + placeholder={size} + help={__( + 'Height of the blob shape. Defaults to width if empty.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + !!maxWidth} + onDeselect={() => + setAttributes({ maxWidth: undefined }) + } + isShownByDefault + > + + setAttributes({ + maxWidth: value || undefined, + }) + } + units={[ + { value: 'px', label: 'px', default: 800 }, + { value: '%', label: '%', default: 100 }, + { value: 'vw', label: 'vw', default: 60 }, + { value: 'rem', label: 'rem', default: 50 }, + ]} + help={__( + 'Constrain the blob wrapper width and center it. Leave empty for no constraint.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + blobAnimation !== 'none'} + onDeselect={() => + setAttributes({ blobAnimation: 'none' }) + } + isShownByDefault + > + + setAttributes({ blobAnimation: value }) + } + help={__( + 'Float/Pulse/Spin keep your shape. Morph changes the shape itself.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + animationDuration !== '8s'} + onDeselect={() => + setAttributes({ animationDuration: '8s' }) + } + isShownByDefault + > + + setAttributes({ + animationDuration: value || '8s', + }) + } + units={[ + { value: 's', label: 's', default: 8 }, + { value: 'ms', label: 'ms', default: 8000 }, + ]} + min={1} + max={30} + help={__( + 'How long one animation cycle takes', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + animationEasing !== 'ease-in-out'} + onDeselect={() => + setAttributes({ animationEasing: 'ease-in-out' }) + } + isShownByDefault + > + + setAttributes({ animationEasing: value }) + } + help={__( + 'Animation timing function', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {enableOverlay && ( + overlayOpacity !== 80} + onDeselect={() => + setAttributes({ overlayOpacity: 80 }) + } + isShownByDefault + > + + setAttributes({ overlayOpacity: value }) + } + min={0} + max={100} + help={__( + 'Overlay transparency (0 = transparent, 100 = opaque)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + + + + { + // Auto-enable overlay when user sets a color + if (color) { + setAttributes({ + overlayColor: encodeColorValue( + color, + colorGradientSettings + ), + enableOverlay: true, + }); + } else { + // Disable overlay when color is cleared + setAttributes({ + overlayColor: '', + enableOverlay: false, + }); + } + }, + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + +
    +
    + {enableOverlay && ( +
    + )} +
    +
    +
    +
    +
    + + ); +} diff --git a/src/blocks/blobs/editor.scss b/src/blocks/blobs/editor.scss new file mode 100644 index 0000000..cbdda09 --- /dev/null +++ b/src/blocks/blobs/editor.scss @@ -0,0 +1,74 @@ +/** + * Blobs Block - Editor Styles + * + * Editor-specific styles - main styles in style.scss + * Keep minimal to ensure editor matches frontend + * + * @since 1.0.0 + */ + +// Editor: Wrapper styling +.editor-styles-wrapper .airo-wp-blobs-wrapper { + // Ensure wrapper behavior matches frontend + display: block; + width: 100%; + // Make wrapper transparent so backgrounds show on blob + // Repeat class for higher specificity (0,3,0) to beat WordPress's + // color utility classes like .has-{slug}-background-color which also use !important + &.airo-wp-blobs-wrapper { + background: none !important; + background-color: transparent !important; + } + + // CRITICAL: Alignment classes on wrapper should apply to inner blob (must match style.scss) + &.aligncenter > .airo-wp-blobs { + margin-left: auto; + margin-right: auto; + } + + &.alignleft > .airo-wp-blobs { + margin-right: auto; + } + + &.alignright > .airo-wp-blobs { + margin-left: auto; + } + + // Native max-width constraint (must match style.scss). Mirrors the + // frontend kit-controllable var chain so the editor preview matches. + &.airo-wp-has-max-width { + max-width: var(--airo-wp-blob-max-width, var(--wp--custom--airo-wp--blobs--max-width, 100%)); + margin-left: auto; + margin-right: auto; + + // Align-aware anchoring — must match style.scss. + &.alignleft, + &.has-text-align-left { + margin-left: 0; + margin-right: auto; + } + + &.alignright, + &.has-text-align-right { + margin-left: auto; + margin-right: 0; + } + } +} + +// Editor: Enable blob animations for WYSIWYG editing +.editor-styles-wrapper .airo-wp-blobs { + // Animations work the same in editor as frontend + // No special overrides needed - style.scss handles everything + + // Optional: Add subtle border for better visibility in editor + &__shape { + outline: 1px dashed rgba(0, 0, 0, 0.1); + outline-offset: -1px; + + // Remove outline on hover + &:hover { + outline-color: transparent; + } + } +} diff --git a/src/blocks/blobs/index.js b/src/blocks/blobs/index.js new file mode 100644 index 0000000..6c7cb8c --- /dev/null +++ b/src/blocks/blobs/index.js @@ -0,0 +1,40 @@ +/** + * Blobs Block + * + * Create random, unique, and organic-looking blob shapes with animations. + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import './style.scss'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +/** + * Custom icon for Blobs block + */ +const icon = ( + + + +); + +/** + * Register the Blobs block + */ +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/blobs/save.js b/src/blocks/blobs/save.js new file mode 100644 index 0000000..4c6c03f --- /dev/null +++ b/src/blocks/blobs/save.js @@ -0,0 +1,85 @@ +/** + * Blobs Block - Save Component + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import { hasExplicitString } from '../../utils/has-explicit-value'; + +export default function BlobsSave({ attributes }) { + const { + blobShape, + blobAnimation, + animationDuration, + animationEasing, + size, + height, + maxWidth, + enableOverlay, + overlayColor, + overlayOpacity, + } = attributes; + + // Same classes as edit.js - MUST MATCH + const blobClasses = classnames('airo-wp-blobs', { + [`airo-wp-blobs--${blobShape}`]: blobShape, + [`airo-wp-blobs--${blobAnimation}`]: + blobAnimation && blobAnimation !== 'none', + }); + + // Apply animation settings as CSS custom properties - MUST MATCH edit.js + const customStyles = { + '--airo-wp-blob-size': size, + ...(height ? { '--airo-wp-blob-height': height } : {}), + '--airo-wp-blob-animation-duration': animationDuration, + '--airo-wp-blob-animation-easing': animationEasing, + }; + + // Optional max-width constraint on the wrapper - MUST MATCH edit.js. + // Emitted as a kit-controllable CSS custom property so themes/kits can + // retheme via --airo-wp-blob-max-width or the blobs max-width token; the raw + // max-width and centering margins live in the stylesheet, not inline. The + // class + var are only added when the author sets an explicit maxWidth, so + // unset blobs serialize byte-identically to the pre-maxWidth output. + const hasMaxWidth = hasExplicitString(maxWidth); + + // Get block props with our wrapper class + const blockProps = useBlockProps.save({ + className: classnames('airo-wp-blobs-wrapper', { + 'airo-wp-has-max-width': hasMaxWidth, + }), + ...(hasMaxWidth && { + style: { '--airo-wp-blob-max-width': maxWidth }, + }), + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-blobs__content', + }); + + return ( +
    +
    + {enableOverlay && ( +
    + )} +
    +
    +
    +
    +
    + ); +} diff --git a/src/blocks/blobs/style.scss b/src/blocks/blobs/style.scss new file mode 100644 index 0000000..d6aae1e --- /dev/null +++ b/src/blocks/blobs/style.scss @@ -0,0 +1,294 @@ +/** + * Blobs Block - Frontend Styles + * + * Organic blob shapes with CSS border-radius morphing animations + * + * @since 1.0.0 + */ + +// Outer wrapper - takes full width and receives WordPress classes/styles +.airo-wp-blobs-wrapper { + display: block; + width: 100%; + // WordPress applies backgrounds here, but we want them on the blob + // So we make wrapper transparent and JavaScript transfers styles to inner blob + // Repeated class for higher specificity (0,2,0) to beat WordPress's + // color utility classes like .has-{slug}-background-color which also use !important + &.airo-wp-blobs-wrapper { + background: none !important; + background-color: transparent !important; + } + + // Alignment classes on wrapper should apply to inner blob + &.aligncenter > .airo-wp-blobs { + margin-left: auto; + margin-right: auto; + } + + &.alignleft > .airo-wp-blobs { + margin-right: auto; + } + + &.alignright > .airo-wp-blobs { + margin-left: auto; + } + + // Native, kit-controllable max-width constraint. The class + inline + // --airo-wp-blob-max-width are only emitted when the author sets an explicit + // maxWidth; the actual max-width and centering margins live here so themes + // and Style Kits can retheme the value. Resolution order: + // 1. --airo-wp-blob-max-width — inline author value / Style Kit CSS hook. + // 2. --wp--custom--airo-wp--blobs--max-width — theme.json token. + // 3. 100% — literal fallback (no constraint). + &.airo-wp-has-max-width { + max-width: var(--airo-wp-blob-max-width, var(--wp--custom--airo-wp--blobs--max-width, 100%)); + margin-left: auto; + margin-right: auto; + + // Anchor the constrained box to the block's alignment instead of always + // centering, preserving the behavior of the generic max-width extension + // this replaces. Keyed on the align classes WordPress puts on the + // wrapper (alignleft/alignright) and core's text-align classes + // (has-text-align-*), so either control anchors the box. The extra class + // raises specificity above the centering rule, so no !important needed. + &.alignleft, + &.has-text-align-left { + margin-left: 0; + margin-right: auto; + } + + &.alignright, + &.has-text-align-right { + margin-left: auto; + margin-right: 0; + } + } +} + +.airo-wp-blobs { + // CSS Custom Properties (set from block attributes) + --airo-wp-blob-size: 300px; + --airo-wp-blob-animation-duration: 8s; + --airo-wp-blob-animation-easing: ease-in-out; + + // Container with blob shape + position: relative; + width: var(--airo-wp-blob-size); + height: var(--airo-wp-blob-height, var(--airo-wp-blob-size)); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + + // Default background color (applied via JavaScript if no user color is set) + // Don't set here - would override user's inline background-color styles + + // Shape variants - EXTREMELY different organic blob shapes + &--shape-1 { + // Classic blob - balanced and organic + border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; + } + + &--shape-2 { + // Amoeba - very irregular, super elongated + border-radius: 15% 85% 85% 15% / 80% 20% 80% 20%; + } + + &--shape-3 { + // Pebble - almost perfect circle + border-radius: 50% 50% 50% 50% / 50% 50% 50% 50%; + } + + &--shape-4 { + // Splash - EXTREME asymmetry + border-radius: 10% 90% 20% 80% / 90% 10% 85% 15%; + } + + &--shape-5 { + // Drop - sharp teardrop with point at bottom + border-radius: 50% 50% 0% 100% / 60% 60% 0% 100%; + } + + &--shape-6 { + // Cloud - very irregular soft blob + border-radius: 75% 25% 65% 35% / 35% 65% 25% 75%; + } + + // Background overlay (for images) + &__overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; + } + + // Blob shape container + &__shape { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + // Content inside blob + .airo-wp-blobs__content { + position: relative; + z-index: 2; + padding: var(--wp--preset--spacing--medium, 1.5rem); + text-align: center; + width: 100%; + + // Ensure proper spacing for block elements + > * + * { + margin-top: var(--wp--preset--spacing--small, 1rem); + } + } + } + + // Animation variants (applied to outer container) + &--morph-1 { + animation: airo-wp-blob-morph-1 var(--airo-wp-blob-animation-duration) var(--airo-wp-blob-animation-easing) infinite; + } + + &--morph-2 { + animation: airo-wp-blob-morph-2 var(--airo-wp-blob-animation-duration) var(--airo-wp-blob-animation-easing) infinite; + } + + &--float { + animation: airo-wp-blob-float var(--airo-wp-blob-animation-duration) var(--airo-wp-blob-animation-easing) infinite; + } + + &--pulse { + animation: airo-wp-blob-pulse var(--airo-wp-blob-animation-duration) var(--airo-wp-blob-animation-easing) infinite; + } + + &--spin { + animation: airo-wp-blob-spin var(--airo-wp-blob-animation-duration) linear infinite; + } +} + +// Keyframe Animations + +// Morph 1: Organic shape morphing through 3 states +@keyframes airo-wp-blob-morph-1 { + + 0%, 100% { + border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; + } + + 33% { + border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; + } + + 66% { + border-radius: 50% 40% 20% 80% / 25% 70% 30% 75%; + } +} + +// Morph 2: Different morphing pattern +@keyframes airo-wp-blob-morph-2 { + + 0%, 100% { + border-radius: 40% 60% 60% 40% / 70% 30% 70% 30%; + } + + 25% { + border-radius: 70% 30% 50% 50% / 30% 30% 70% 70%; + } + + 50% { + border-radius: 50% 50% 30% 70% / 50% 50% 50% 50%; + } + + 75% { + border-radius: 30% 70% 70% 30% / 60% 40% 60% 40%; + } +} + +// Float: Gentle floating motion (no rotation) +@keyframes airo-wp-blob-float { + + 0%, 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-15px); + } +} + +// Pulse: Scale in and out with slight rotation +@keyframes airo-wp-blob-pulse { + + 0%, 100% { + transform: scale(1) rotate(0deg); + } + + 50% { + transform: scale(1.1) rotate(5deg); + } +} + +// Spin: Continuous rotation with slight scale +@keyframes airo-wp-blob-spin { + + 0% { + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(180deg) scale(1.05); + } + + 100% { + transform: rotate(360deg) scale(1); + } +} + +// Accessibility: Reduce motion for users who prefer it +@media (prefers-reduced-motion: reduce) { + + .airo-wp-blobs { + + &--morph-1, + &--morph-2, + &--float, + &--pulse, + &--spin { + animation: none; + } + } +} + +// Responsive: Adjust size on mobile +@media (max-width: 768px) { + + .airo-wp-blobs { + + &__shape { + // Scale down on mobile if using fixed pixel values + max-width: 100%; + max-height: 100vw; + } + } +} + +// Print styles: Remove animations +@media print { + + .airo-wp-blobs { + + &--morph-1, + &--morph-2, + &--float, + &--pulse, + &--spin { + animation: none; + transform: none; + } + } +} diff --git a/src/blocks/blobs/test/deprecated.test.js b/src/blocks/blobs/test/deprecated.test.js new file mode 100644 index 0000000..fe2dd11 --- /dev/null +++ b/src/blocks/blobs/test/deprecated.test.js @@ -0,0 +1,192 @@ +/** + * Blobs Block - Native max-width control + migration tests + * + * Verifies: + * - a blob WITHOUT maxWidth serializes exactly as before (no + * `airo-wp-has-max-width` class, no `--airo-wp-blob-max-width` var) and re-parses + * valid against the current save() without any migration; + * - a blob WITH maxWidth emits the `airo-wp-has-max-width` class and the + * kit-controllable `--airo-wp-blob-max-width` custom property (not a raw inline + * max-width); + * - OLD content that got its max-width from the generic max-width extension + * (a `dsgoMaxWidth` attribute + `airo-wp-has-max-width` class + raw inline + * `max-width;margin-left:auto;margin-right:auto`) migrates silently to the + * native `maxWidth` via the v3 deprecation instead of showing WordPress's + * "Attempt Recovery" warning. + */ + +import { + registerBlockType, + setCategories, + parse, + createBlock, + serialize, + getBlockContent, + // eslint-disable-next-line import/no-unresolved +} from '@wordpress/block-editor/node_modules/@wordpress/blocks'; +import metadata from '../block.json'; +import save from '../save'; +import deprecated from '../deprecated'; + +setCategories([{ slug: 'airo-wp', title: 'airo-wp' }]); + +registerBlockType(metadata.name, { ...metadata, save, deprecated }); + +// deprecated.js exports newest-first: [v3, v2, v1]. +const [v3Deprecation] = deprecated; + +describe('blobs save() - native max-width', () => { + test('a blob without maxWidth omits the class and the var', () => { + const markup = serialize( + createBlock(metadata.name, { + size: '300px', + }) + ); + expect(markup).toContain('airo-wp-blobs-wrapper'); + expect(markup).not.toContain('airo-wp-has-max-width'); + expect(markup).not.toContain('--airo-wp-blob-max-width'); + }); + + test('a blob with maxWidth adds the class and the kit-controllable var', () => { + const markup = serialize( + createBlock(metadata.name, { + size: '80%', + maxWidth: '800px', + }) + ); + expect(markup).toContain('airo-wp-has-max-width'); + expect(markup).toContain('--airo-wp-blob-max-width:800px'); + // The raw max-width / centering margins live in the stylesheet, so they + // must NOT be written inline on the wrapper. + expect(markup).not.toMatch(/(?:^|[^-])max-width:/); + expect(markup).not.toContain('margin-left:auto'); + }); + + test('a left/right-aligned blob with maxWidth keeps its align class alongside airo-wp-has-max-width', () => { + // The align-aware anchoring is CSS-only (style.scss keys margins off the + // alignleft/alignright classes WordPress adds to the wrapper). This guards + // that both classes are present on the same element so the anchoring hook + // can apply — i.e. a left/right-aligned + max-width blob is not silently + // re-centered. + const left = serialize( + createBlock(metadata.name, { align: 'left', maxWidth: '800px' }) + ); + expect(left).toContain('alignleft'); + expect(left).toContain('airo-wp-has-max-width'); + + const right = serialize( + createBlock(metadata.name, { align: 'right', maxWidth: '800px' }) + ); + expect(right).toContain('alignright'); + expect(right).toContain('airo-wp-has-max-width'); + }); + + test('a maxWidth-unset blob re-parses valid with no migration', () => { + const markup = serialize( + createBlock(metadata.name, { + size: '400px', + blobShape: 'shape-2', + blobAnimation: 'morph-1', + }) + ); + const [block] = parse(markup); + expect(block.name).toBe('airo-wp/blobs'); + expect(block.isValid).toBe(true); + expect(block.attributes.maxWidth).toBeUndefined(); + }); +}); + +describe('blobs deprecations - v3 native max-width migration', () => { + // Derive byte-exact OLD (extension-era) markup from the current canonical + // output: the extension stored `dsgoMaxWidth` (not `maxWidth`) and wrote a + // raw inline max-width + centering margins instead of the var. + const canonical = serialize( + createBlock(metadata.name, { + align: 'center', + size: '80%', + maxWidth: '800px', + }) + ); + const OLD_MARKUP = canonical + .replace('"maxWidth":"800px"', '"dsgoMaxWidth":"800px"') + .replace( + '--airo-wp-blob-max-width:800px', + 'max-width:800px;margin-left:auto;margin-right:auto' + ); + + test('derived old markup differs from canonical as expected', () => { + expect(OLD_MARKUP).toContain('airo-wp-has-max-width'); + expect(OLD_MARKUP).toContain('"dsgoMaxWidth":"800px"'); + expect(OLD_MARKUP).toMatch(/(?:^|[^-])max-width:800px/); + expect(OLD_MARKUP).not.toContain('--airo-wp-blob-max-width'); + }); + + test('old extension blob migrates silently to native maxWidth', () => { + const [block] = parse(OLD_MARKUP); + + expect(console).toHaveInformed(); + expect(block.name).toBe('airo-wp/blobs'); + expect(block.isValid).toBe(true); + expect(block.attributes.maxWidth).toBe('800px'); + expect(block.attributes.dsgoMaxWidth).toBeUndefined(); + // Re-serialized with the current save(): class + kit var, no raw inline. + const content = getBlockContent(block); + expect(content).toContain('airo-wp-has-max-width'); + expect(content).toContain('--airo-wp-blob-max-width:800px'); + expect(content).not.toMatch(/(?:^|[^-])max-width:/); + }); + + test('v3 isEligible flags old extension markup', () => { + expect( + v3Deprecation.isEligible({}, [], { + blockNode: { innerHTML: OLD_MARKUP }, + }) + ).toBe(true); + }); + + test('v3 isEligible ignores current native markup', () => { + expect( + v3Deprecation.isEligible({}, [], { + blockNode: { innerHTML: canonical }, + }) + ).toBe(false); + }); + + test('v3 isEligible ignores a plain (no max-width) blob', () => { + const plain = serialize(createBlock(metadata.name, { size: '300px' })); + expect( + v3Deprecation.isEligible({}, [], { + blockNode: { innerHTML: plain }, + }) + ).toBe(false); + }); + + test('v3 isEligible ignores a native blob whose NESTED child uses the generic max-width extension', () => { + // Regression: a valid native Blobs block (own maxWidth via + // --airo-wp-blob-max-width on the wrapper root) containing a nested child + // that the still-active generic extension stamped with the same + // airo-wp-has-max-width class + a raw inline max-width. The old-format + // signature lives only on the nested child, not the wrapper's own + // opening tag, so it must NOT be flagged (else its maxWidth would be + // silently dropped and the block marked invalid). + const nestedHTML = + '
    ' + + '
    ' + + '

    Nested

    ' + + '
    '; + expect( + v3Deprecation.isEligible({}, [], { + blockNode: { innerHTML: nestedHTML }, + }) + ).toBe(false); + }); + + test('v3 migrate maps dsgoMaxWidth onto maxWidth and drops the legacy key', () => { + expect( + v3Deprecation.migrate({ dsgoMaxWidth: '800px', size: '80%' }) + ).toEqual({ maxWidth: '800px', size: '80%' }); + expect( + v3Deprecation.migrate({ dsgoMaxWidth: '', size: '80%' }) + ).toEqual({ size: '80%' }); + }); +}); diff --git a/src/blocks/blobs/view.js b/src/blocks/blobs/view.js new file mode 100644 index 0000000..e119454 --- /dev/null +++ b/src/blocks/blobs/view.js @@ -0,0 +1,162 @@ +/** + * Blobs Block - Frontend JavaScript + * + * Handles accessibility and animation initialization + * + * @since 1.0.0 + */ + +/** + * Transfer background styles from wrapper to blob + */ +function transferBackgroundStyles() { + const wrappers = document.querySelectorAll('.airo-wp-blobs-wrapper'); + + wrappers.forEach((wrapper) => { + const blob = wrapper.querySelector('.airo-wp-blobs'); + if (!blob) { + return; + } + + // WordPress sets inline styles on the wrapper + // We need to read inline styles directly because our CSS has `background: none !important;` + // which overrides them in getComputedStyle() + const inlineStyle = wrapper.style; + + // Transfer background image + if ( + inlineStyle.backgroundImage && + inlineStyle.backgroundImage !== 'none' + ) { + blob.style.setProperty( + 'background-image', + inlineStyle.backgroundImage + ); + } + + // Transfer background size + if ( + inlineStyle.backgroundSize && + inlineStyle.backgroundSize !== 'auto' + ) { + blob.style.setProperty( + 'background-size', + inlineStyle.backgroundSize + ); + } + + // Transfer background position + if (inlineStyle.backgroundPosition) { + blob.style.setProperty( + 'background-position', + inlineStyle.backgroundPosition + ); + } + + // Transfer background repeat + if ( + inlineStyle.backgroundRepeat && + inlineStyle.backgroundRepeat !== 'repeat' + ) { + blob.style.setProperty( + 'background-repeat', + inlineStyle.backgroundRepeat + ); + } + + // Transfer background attachment + if ( + inlineStyle.backgroundAttachment && + inlineStyle.backgroundAttachment !== 'scroll' + ) { + blob.style.setProperty( + 'background-attachment', + inlineStyle.backgroundAttachment + ); + } + + // Transfer WordPress background color classes from wrapper to blob + // so WordPress's own CSS applies the color to the blob shape directly + const bgClasses = Array.from(wrapper.classList).filter( + (c) => + c.match(/^has-.*-background-color$/) || c === 'has-background' + ); + bgClasses.forEach((cls) => { + wrapper.classList.remove(cls); + blob.classList.add(cls); + }); + + // Transfer inline background color (custom non-preset colors) + if (inlineStyle.backgroundColor) { + blob.style.setProperty( + 'background-color', + inlineStyle.backgroundColor + ); + } else if (bgClasses.length === 0) { + // Apply default color if no user color is set + const defaultColor = + window + .getComputedStyle(document.documentElement) + .getPropertyValue('--wp--preset--color--accent-2') || + '#2563eb'; + blob.style.setProperty('background-color', defaultColor.trim()); + } + }); +} + +/** + * Initialize blob animations with accessibility support + */ +function initBlobs() { + // First transfer background styles + transferBackgroundStyles(); + + const blobs = document.querySelectorAll('.airo-wp-blobs'); + + if (blobs.length === 0) { + return; + } + + // Check if user prefers reduced motion + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; + + if (prefersReducedMotion) { + // Disable all animations by adding a class + blobs.forEach((blob) => { + blob.classList.add('airo-wp-blobs--no-animation'); + }); + return; + } + + // Initialize each blob + blobs.forEach(() => { + // Future: Add SVG-based animation logic here if implementing Phase 2 + // For now, CSS animations handle everything via data-blob-animation attribute + }); +} + +// Initialize on DOMContentLoaded or immediately if DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initBlobs); +} else { + initBlobs(); +} + +// Re-initialize after soft navigation (bfcache, AJAX) +document.addEventListener('airo-wp-content-loaded', initBlobs); + +// Re-initialize if user changes motion preferences +if (window.matchMedia) { + const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + + // Modern browsers + if (motionQuery.addEventListener) { + motionQuery.addEventListener('change', initBlobs); + } + // Legacy browsers + else if (motionQuery.addListener) { + motionQuery.addListener(initBlobs); + } +} diff --git a/src/blocks/breadcrumbs/block.json b/src/blocks/breadcrumbs/block.json new file mode 100644 index 0000000..ea8a27b --- /dev/null +++ b/src/blocks/breadcrumbs/block.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/breadcrumbs", + "version": "1.0.0", + "title": "Breadcrumbs", + "category": "airo-wp", + "description": "Display navigation breadcrumbs with Schema.org markup for improved UX and SEO.", + "keywords": [ + "breadcrumb", + "navigation", + "hierarchy", + "seo" + ], + "textdomain": "airo-wp", + "icon": "arrow-right-alt2", + "usesContext": [ + "postId", + "postType" + ], + "attributes": { + "showHome": { + "type": "boolean", + "default": true + }, + "homeText": { + "type": "string", + "default": "Home" + }, + "separator": { + "type": "string", + "default": "slash", + "enum": [ + "slash", + "chevron", + "greater", + "bullet", + "arrow-right" + ] + }, + "showCurrent": { + "type": "boolean", + "default": true + }, + "linkCurrent": { + "type": "boolean", + "default": false + }, + "prefixText": { + "type": "string", + "default": "" + }, + "hideOnHome": { + "type": "boolean", + "default": true + }, + "contentJustification": { + "type": "string", + "default": "left", + "enum": [ + "left", + "center", + "right" + ] + } + }, + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true + }, + "color": { + "text": true, + "link": true, + "background": true, + "__experimentalDefaultControls": { + "link": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + }, + "__experimentalFontWeight": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "example": { + "attributes": { + "showHome": true, + "homeText": "Home", + "separator": "chevron", + "showCurrent": true, + "linkCurrent": false + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "viewScript": "file:./view.js", + "render": "file:./render.php" +} diff --git a/src/blocks/breadcrumbs/components/InspectorPanels.js b/src/blocks/breadcrumbs/components/InspectorPanels.js new file mode 100644 index 0000000..b71f1df --- /dev/null +++ b/src/blocks/breadcrumbs/components/InspectorPanels.js @@ -0,0 +1,170 @@ +/** + * Inspector Control Panels for Breadcrumbs Block + * + * Renders DsgoInspectorPanel.Item entries for the breadcrumbs display + * attributes. Meant to be composed inside the Settings DsgoInspectorPanel + * in breadcrumbs/edit.js. + */ +import { __ } from '@wordpress/i18n'; +import { + ToggleControl, + TextControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../components/shared'; + +export function DisplaySettingsPanel({ attributes, setAttributes }) { + const { + showHome, + homeText, + separator, + showCurrent, + linkCurrent, + prefixText, + hideOnHome, + } = attributes; + + return ( + <> + showHome !== true} + onDeselect={() => setAttributes({ showHome: true })} + isShownByDefault + > + setAttributes({ showHome: value })} + help={__( + 'Display a link to the homepage at the start of the breadcrumb trail', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + {showHome && ( + homeText !== 'Home'} + onDeselect={() => setAttributes({ homeText: 'Home' })} + isShownByDefault + > + setAttributes({ homeText: value })} + help={__( + 'Text to display for the home link', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + )} + + separator !== 'slash'} + onDeselect={() => setAttributes({ separator: 'slash' })} + isShownByDefault + > + ', value: 'greater' }, + { label: '•', value: 'bullet' }, + { label: '→', value: 'arrow-right' }, + ]} + onChange={(value) => setAttributes({ separator: value })} + help={__( + 'Character used to separate breadcrumb items', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + showCurrent !== true} + onDeselect={() => setAttributes({ showCurrent: true })} + isShownByDefault + > + setAttributes({ showCurrent: value })} + help={__( + 'Display the current page in the breadcrumb trail', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + {showCurrent && ( + linkCurrent !== false} + onDeselect={() => setAttributes({ linkCurrent: false })} + isShownByDefault + > + + setAttributes({ linkCurrent: value }) + } + help={__( + 'Make the current page a clickable link', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + )} + + prefixText !== ''} + onDeselect={() => setAttributes({ prefixText: '' })} + isShownByDefault + > + setAttributes({ prefixText: value })} + help={__( + 'Optional text to display before the breadcrumb trail (e.g., "You are here:")', + 'airo-wp' + )} + placeholder={__('You are here:', 'airo-wp')} + __nextHasNoMarginBottom + /> + + + hideOnHome !== true} + onDeselect={() => setAttributes({ hideOnHome: true })} + isShownByDefault + > + setAttributes({ hideOnHome: value })} + help={__( + 'Hide breadcrumbs when viewing the homepage', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + ); +} diff --git a/src/blocks/breadcrumbs/edit.js b/src/blocks/breadcrumbs/edit.js new file mode 100644 index 0000000..57dd180 --- /dev/null +++ b/src/blocks/breadcrumbs/edit.js @@ -0,0 +1,284 @@ +/** + * Breadcrumbs Block - Edit Component + * + * Shows a preview of breadcrumbs in the editor based on current page context. + */ +/* eslint-disable no-nested-ternary, jsx-a11y/anchor-is-valid */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + InspectorControls, + BlockControls, + JustifyContentControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { Notice } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import classnames from 'classnames'; +import { useMemo, Fragment } from '@wordpress/element'; +import { DisplaySettingsPanel } from './components/InspectorPanels'; +import { useBreadcrumbData } from './hooks/useBreadcrumbData'; + +/** + * Get separator character based on attributes + * + * @param {string} separator - Separator type + * @return {string} Separator character + */ +function getSeparatorChar(separator) { + const separators = { + slash: '/', + chevron: '›', + greater: '>', + bullet: '•', + 'arrow-right': '→', + }; + return separators[separator] || '/'; +} + +export default function Edit({ attributes, setAttributes, clientId }) { + const { + showHome, + homeText, + separator, + showCurrent, + linkCurrent, + prefixText, + contentJustification, + } = attributes; + + // Get color settings for ColorGradientSettingsDropdown + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Get current post/page data from editor + const { postTitle, postType, postParents, postCategories, isEditorReady } = + useBreadcrumbData(); + + // Build preview breadcrumb trail + const previewTrail = useMemo(() => { + const trail = []; + + // Add home link (always a link) + if (showHome) { + trail.push({ + title: homeText, + isCurrent: false, + }); + } + + // Add parent pages for pages (always links) + if (postType === 'page' && postParents.length > 0) { + postParents.forEach((parent) => { + trail.push({ + title: parent.title, + isCurrent: false, + }); + }); + } + + // Add categories for posts (always links) + if (postType === 'post' && postCategories.length > 0) { + postCategories.forEach((category) => { + trail.push({ + title: category.title, + isCurrent: false, + }); + }); + } + + // Add current page (link only if linkCurrent is true) + if (showCurrent && postTitle) { + trail.push({ + title: postTitle, + isCurrent: true, + linked: linkCurrent, + }); + } + + return trail; + }, [ + showHome, + homeText, + showCurrent, + linkCurrent, + postTitle, + postType, + postParents, + postCategories, + ]); + + const separatorChar = getSeparatorChar(separator); + + const justifyClass = + contentJustification && contentJustification !== 'left' + ? `is-content-justification-${contentJustification}` + : ''; + + const blockProps = useBlockProps({ + className: classnames('airo-wp-breadcrumbs', justifyClass), + }); + + return ( + <> + + + setAttributes({ contentJustification: value ?? 'left' }) + } + /> + + + + + setAttributes({ + showHome: true, + homeText: 'Home', + separator: 'slash', + showCurrent: true, + linkCurrent: false, + prefixText: '', + hideOnHome: true, + }) + } + > + + + + + + { + setAttributes({ + style: { + ...attributes.style, + elements: { + ...attributes.style?.elements, + link: { + ...attributes.style?.elements + ?.link, + color: { + text: newColor, + }, + }, + }, + }, + }); + }, + enableAlpha: true, + }, + { + label: __('Link Hover', 'airo-wp'), + colorValue: + attributes.style?.elements?.link?.[':hover'] + ?.color?.text, + onColorChange: (newColor) => { + setAttributes({ + style: { + ...attributes.style, + elements: { + ...attributes.style?.elements, + link: { + ...attributes.style?.elements + ?.link, + ':hover': { + color: { + text: newColor, + }, + }, + }, + }, + }, + }); + }, + enableAlpha: true, + }, + ]} + panelId={clientId} + {...colorGradientSettings} + /> + + + + + ); +} diff --git a/src/blocks/breadcrumbs/editor.scss b/src/blocks/breadcrumbs/editor.scss new file mode 100644 index 0000000..edcc8d0 --- /dev/null +++ b/src/blocks/breadcrumbs/editor.scss @@ -0,0 +1,17 @@ +/** + * Breadcrumbs Block - Editor Styles + * + * Editor-only styles for the breadcrumbs block. + */ + +.airo-wp-breadcrumbs { + // Notice component styling in editor + .components-notice { + margin: 0; + padding: 0.75em 1em; + + &__content { + margin: 0; + } + } +} diff --git a/src/blocks/breadcrumbs/hooks/useBreadcrumbData.js b/src/blocks/breadcrumbs/hooks/useBreadcrumbData.js new file mode 100644 index 0000000..2f90637 --- /dev/null +++ b/src/blocks/breadcrumbs/hooks/useBreadcrumbData.js @@ -0,0 +1,113 @@ +/** + * Custom hook to fetch breadcrumb data from the editor + * + * @return {Object} Breadcrumb data including post title, type, parents, and categories + */ +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; + +const EMPTY_ARRAY = []; + +export function useBreadcrumbData() { + const rawData = useSelect((select) => { + const { getCurrentPost, getEditedPostAttribute } = + select('core/editor') || {}; + const { getEntityRecords } = select('core') || {}; + + if (!getCurrentPost || !getEditedPostAttribute) { + return { + postTitle: '', + postType: 'post', + parentId: 0, + categoryIds: EMPTY_ARRAY, + allPages: null, + allCategories: null, + isEditorReady: false, + }; + } + + const title = getEditedPostAttribute('title'); + const type = getEditedPostAttribute('type'); + const parentId = getEditedPostAttribute('parent'); + const categoryIds = getEditedPostAttribute('categories') || EMPTY_ARRAY; + + let allPages = null; + let allCategories = null; + + if (type === 'page' && parentId && getEntityRecords) { + allPages = getEntityRecords('postType', 'page', { + per_page: -1, + }); + } + + if (type === 'post' && categoryIds.length > 0 && getEntityRecords) { + allCategories = getEntityRecords('taxonomy', 'category', { + per_page: -1, + }); + } + + return { + postTitle: title || __('(no title)', 'airo-wp'), + postType: type || 'post', + parentId: parentId || 0, + categoryIds, + allPages, + allCategories, + isEditorReady: true, + }; + }, []); + + const { + postTitle, + postType, + parentId, + categoryIds, + allPages, + allCategories, + isEditorReady, + } = rawData; + + const postParents = useMemo(() => { + if (postType !== 'page' || !parentId || !allPages) { + return EMPTY_ARRAY; + } + const parents = []; + let currentParentId = parentId; + while (currentParentId) { + const parent = allPages.find((p) => p.id === currentParentId); + if (parent) { + parents.unshift({ + title: parent.title.rendered, + id: parent.id, + }); + currentParentId = parent.parent; + } else { + break; + } + } + return parents.length > 0 ? parents : EMPTY_ARRAY; + }, [postType, parentId, allPages]); + + const postCategories = useMemo(() => { + if (postType !== 'post' || categoryIds.length === 0 || !allCategories) { + return EMPTY_ARRAY; + } + const firstCategoryId = categoryIds[0]; + const category = allCategories.find( + (cat) => cat.id === firstCategoryId + ); + if (category) { + return [{ title: category.name, id: category.id }]; + } + return EMPTY_ARRAY; + }, [postType, categoryIds, allCategories]); + + return { + postTitle, + postType, + postParents, + postCategories, + isEditorReady, + }; +} diff --git a/src/blocks/breadcrumbs/index.js b/src/blocks/breadcrumbs/index.js new file mode 100644 index 0000000..ebec78a --- /dev/null +++ b/src/blocks/breadcrumbs/index.js @@ -0,0 +1,50 @@ +/** + * Breadcrumbs Block - Registration + * + * Registers the Breadcrumbs block with WordPress. + * Provides dynamic breadcrumb navigation with Schema.org markup. + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +/** + * Register Breadcrumbs Block + * + * Display navigation breadcrumbs showing page hierarchy with + * Schema.org markup for improved SEO and user experience. + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, +}); diff --git a/src/blocks/breadcrumbs/render.php b/src/blocks/breadcrumbs/render.php new file mode 100644 index 0000000..8a8f0ab --- /dev/null +++ b/src/blocks/breadcrumbs/render.php @@ -0,0 +1,91 @@ + implode( ' ', $classes ), + 'aria-label' => __( 'Breadcrumb', 'airo-wp' ), + 'data-airo-wp-breadcrumbs' => wp_json_encode( $trail ), + ) + ); + + // Output directly (WordPress captures echo'd output from render callbacks). + ?> + { + try { + // Prevent duplicate initialization + if (block.hasAttribute('data-airo-wp-schema-injected')) { + return; + } + block.setAttribute('data-airo-wp-schema-injected', 'true'); + + // Parse breadcrumb data + const breadcrumbData = JSON.parse( + block.getAttribute('data-airo-wp-breadcrumbs') + ); + + if (!breadcrumbData || breadcrumbData.length === 0) { + return; + } + + // Generate and inject Schema.org JSON-LD + injectSchemaMarkup(breadcrumbData); + } catch (error) { + // Silently fail - breadcrumbs still work without Schema.org + // eslint-disable-next-line no-console + console.error('airo-wp Breadcrumbs: Schema.org error', error); + } + }); +} + +/** + * Generate and inject Schema.org BreadcrumbList JSON-LD + * + * @param {Array} breadcrumbs - Array of breadcrumb items + */ +function injectSchemaMarkup(breadcrumbs) { + // Build Schema.org BreadcrumbList structure + const schemaData = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: breadcrumbs.map((item, index) => { + const listItem = { + '@type': 'ListItem', + position: item.position || index + 1, + name: item.title, + }; + + // Add item URL to schema (all breadcrumb items have URLs). + if (item.url) { + listItem.item = item.url; + } + + return listItem; + }), + }; + + // Check if Schema.org markup already exists for breadcrumbs + const existingSchema = document.querySelector( + 'script[type="application/ld+json"][data-airo-wp-breadcrumbs-schema]' + ); + + if (existingSchema) { + // Update existing schema + existingSchema.textContent = JSON.stringify(schemaData); + } else { + // Create new schema script tag + const scriptTag = document.createElement('script'); + scriptTag.type = 'application/ld+json'; + scriptTag.setAttribute('data-airo-wp-breadcrumbs-schema', 'true'); + scriptTag.textContent = JSON.stringify(schemaData); + + // Inject before closing tag (or append to head if body not found) + const targetElement = document.body || document.head; + if (targetElement) { + targetElement.appendChild(scriptTag); + } + } +} + +/** + * Export for potential reuse (if module system is available) + */ +if (typeof module !== 'undefined' && module.exports) { + module.exports = { initBreadcrumbs, injectSchemaMarkup }; +} diff --git a/src/blocks/card/block.json b/src/blocks/card/block.json new file mode 100644 index 0000000..99e4dcf --- /dev/null +++ b/src/blocks/card/block.json @@ -0,0 +1,252 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/card", + "version": "1.0.0", + "title": "Card", + "category": "airo-wp", + "description": "Display content in a card layout with image, badge, title, subtitle, body text, and CTA button. Perfect for pricing, features, services, and team members.", + "keywords": [ + "card", + "content", + "image", + "cta", + "pricing", + "feature" + ], + "textdomain": "airo-wp", + "icon": "id", + "supports": { + "anchor": true, + "align": [ + "left", + "center", + "right", + "wide", + "full" + ], + "html": false, + "inserter": true, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": true, + "blockGap": false + } + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": false + }, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true + }, + "shadow": true, + "interactivity": { + "clientNavigation": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true, + "width": false + } + } + }, + "attributes": { + "layoutPreset": { + "type": "string", + "default": "standard", + "enum": [ + "standard", + "horizontal-left", + "horizontal-right", + "background", + "minimal", + "featured" + ] + }, + "imageId": { + "type": "number", + "default": 0 + }, + "imageUrl": { + "type": "string", + "default": "" + }, + "imageAlt": { + "type": "string", + "default": "" + }, + "imageAspectRatio": { + "type": "string", + "default": "16-9", + "enum": [ + "16-9", + "4-3", + "1-1", + "original", + "custom" + ] + }, + "imageCustomAspectRatio": { + "type": "string", + "default": "" + }, + "imageObjectFit": { + "type": "string", + "default": "cover", + "enum": [ + "cover", + "contain", + "fill", + "scale-down" + ] + }, + "imageFocalPoint": { + "type": "object", + "default": { + "x": 0.5, + "y": 0.5 + } + }, + "badgeText": { + "type": "string", + "default": "" + }, + "badgeStyle": { + "type": "string", + "default": "floating", + "enum": [ + "floating", + "inline" + ] + }, + "badgeFloatingPosition": { + "type": "string", + "default": "top-right", + "enum": [ + "top-left", + "top-right", + "bottom-left", + "bottom-right" + ] + }, + "badgeInlinePosition": { + "type": "string", + "default": "above-title", + "enum": [ + "above-title", + "below-title" + ] + }, + "badgeBackgroundColor": { + "type": "string", + "default": "" + }, + "badgeTextColor": { + "type": "string", + "default": "" + }, + "title": { + "type": "string", + "default": "" + }, + "subtitle": { + "type": "string", + "default": "" + }, + "bodyText": { + "type": "string", + "default": "" + }, + "overlayOpacity": { + "type": "number", + "default": 80 + }, + "overlayColor": { + "type": "string", + "default": "" + }, + "borderColor": { + "type": "string", + "default": "" + }, + "contentAlignment": { + "type": "string", + "default": "center", + "enum": [ + "left", + "center", + "right" + ] + }, + "visualStyle": { + "type": "string", + "default": "default", + "enum": [ + "default", + "outlined", + "filled", + "shadow", + "minimal" + ] + }, + "showImage": { + "type": "boolean", + "default": true + }, + "showTitle": { + "type": "boolean", + "default": true + }, + "showSubtitle": { + "type": "boolean", + "default": true + }, + "showBody": { + "type": "boolean", + "default": true + }, + "showBadge": { + "type": "boolean", + "default": true + }, + "showCta": { + "type": "boolean", + "default": true + } + }, + "providesContext": { + "airo-wp/card/layoutPreset": "layoutPreset" + }, + "example": { + "attributes": { + "title": "Card Title", + "subtitle": "Card Subtitle", + "bodyText": "This is a sample card with all the features you need for pricing, features, services, and team members.", + "badgeText": "New", + "layoutPreset": "standard" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/card/components/inspector/BadgeSettingsPanel.js b/src/blocks/card/components/inspector/BadgeSettingsPanel.js new file mode 100644 index 0000000..8dc73cd --- /dev/null +++ b/src/blocks/card/components/inspector/BadgeSettingsPanel.js @@ -0,0 +1,102 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody, TextControl, SelectControl } from '@wordpress/components'; + +/** + * Badge Settings Panel Component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to set attributes + * @return {Element} Badge settings panel + */ +export default function BadgeSettingsPanel({ attributes, setAttributes }) { + const { + badgeText, + badgeStyle, + badgeFloatingPosition, + badgeInlinePosition, + } = attributes; + + const styleOptions = [ + { label: __('Floating (Over Card)', 'airo-wp'), value: 'floating' }, + { label: __('Inline (In Content)', 'airo-wp'), value: 'inline' }, + ]; + + const floatingPositionOptions = [ + { label: __('Top Left', 'airo-wp'), value: 'top-left' }, + { label: __('Top Right', 'airo-wp'), value: 'top-right' }, + { label: __('Bottom Left', 'airo-wp'), value: 'bottom-left' }, + { label: __('Bottom Right', 'airo-wp'), value: 'bottom-right' }, + ]; + + const inlinePositionOptions = [ + { label: __('Above Title', 'airo-wp'), value: 'above-title' }, + { label: __('Below Title', 'airo-wp'), value: 'below-title' }, + ]; + + return ( + + setAttributes({ badgeText: value })} + placeholder={__('NEW', 'airo-wp')} + help={__('Leave empty to hide the badge.', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + {badgeText && ( + <> + + setAttributes({ badgeStyle: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + {badgeStyle === 'floating' && ( + + setAttributes({ badgeFloatingPosition: value }) + } + help={__( + 'Position the badge over the card.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + )} + + {badgeStyle === 'inline' && ( + + setAttributes({ badgeInlinePosition: value }) + } + help={__( + 'Position the badge in the content flow.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + )} + + )} + + ); +} diff --git a/src/blocks/card/components/inspector/ContentSettingsPanel.js b/src/blocks/card/components/inspector/ContentSettingsPanel.js new file mode 100644 index 0000000..53cdedc --- /dev/null +++ b/src/blocks/card/components/inspector/ContentSettingsPanel.js @@ -0,0 +1,83 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody, ToggleControl } from '@wordpress/components'; + +/** + * Content Settings Panel Component + * Controls which content elements are visible + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to set attributes + * @return {Element} Content settings panel + */ +export default function ContentSettingsPanel({ attributes, setAttributes }) { + const { + showImage, + showTitle, + showSubtitle, + showBody, + showBadge, + showCta, + layoutPreset, + } = attributes; + + return ( + + {layoutPreset !== 'minimal' && ( + setAttributes({ showImage: value })} + help={__('Display the card image.', 'airo-wp')} + __nextHasNoMarginBottom + /> + )} + + setAttributes({ showTitle: value })} + help={__('Display the card title.', 'airo-wp')} + __nextHasNoMarginBottom + /> + + setAttributes({ showSubtitle: value })} + help={__('Display the card subtitle.', 'airo-wp')} + __nextHasNoMarginBottom + /> + + setAttributes({ showBody: value })} + help={__('Display the card body text.', 'airo-wp')} + __nextHasNoMarginBottom + /> + + setAttributes({ showBadge: value })} + help={__('Display the badge element.', 'airo-wp')} + __nextHasNoMarginBottom + /> + + setAttributes({ showCta: value })} + help={__('Display the call-to-action button.', 'airo-wp')} + __nextHasNoMarginBottom + /> + + ); +} diff --git a/src/blocks/card/components/inspector/ImageSettingsPanel.js b/src/blocks/card/components/inspector/ImageSettingsPanel.js new file mode 100644 index 0000000..4018088 --- /dev/null +++ b/src/blocks/card/components/inspector/ImageSettingsPanel.js @@ -0,0 +1,198 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + PanelBody, + SelectControl, + Button, + FocalPointPicker, + TextControl, +} from '@wordpress/components'; +import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; + +/** + * Image Settings Panel Component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to set attributes + * @return {Element} Image settings panel + */ +export default function ImageSettingsPanel({ attributes, setAttributes }) { + const { + imageId, + imageUrl, + imageAlt, + imageAspectRatio, + imageCustomAspectRatio, + imageObjectFit, + imageFocalPoint, + layoutPreset, + } = attributes; + + // Don't show image settings for minimal layout + if (layoutPreset === 'minimal') { + return null; + } + + const aspectRatioOptions = [ + { label: __('16:9 (Landscape)', 'airo-wp'), value: '16-9' }, + { label: __('4:3 (Standard)', 'airo-wp'), value: '4-3' }, + { label: __('1:1 (Square)', 'airo-wp'), value: '1-1' }, + { label: __('Original', 'airo-wp'), value: 'original' }, + { label: __('Custom', 'airo-wp'), value: 'custom' }, + ]; + + const objectFitOptions = [ + { label: __('Cover (Fill Space)', 'airo-wp'), value: 'cover' }, + { label: __('Contain (Fit Inside)', 'airo-wp'), value: 'contain' }, + { label: __('Fill (Stretch)', 'airo-wp'), value: 'fill' }, + { label: __('Scale Down', 'airo-wp'), value: 'scale-down' }, + ]; + + return ( + + + + setAttributes({ + imageId: media.id, + imageUrl: media.url, + imageAlt: media.alt || '', + }) + } + allowedTypes={['image']} + value={imageId} + render={({ open }) => ( +
    + {imageUrl ? ( + <> + {imageAlt} +
    + + +
    + + ) : ( + + )} +
    + )} + /> +
    + + {imageUrl && ( + <> + setAttributes({ imageAlt: value })} + help={__( + 'Describe the image for screen readers and SEO.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + setAttributes({ imageAspectRatio: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + {imageAspectRatio === 'custom' && ( + + setAttributes({ imageCustomAspectRatio: value }) + } + placeholder="16 / 9" + help={__( + 'Enter a CSS aspect-ratio value (e.g., "16 / 9" or "1.5").', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + )} + + + setAttributes({ imageObjectFit: value }) + } + help={__( + 'How the image should fit within its container.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + {imageObjectFit === 'cover' && ( + + setAttributes({ imageFocalPoint: value }) + } + help={__( + 'Click to adjust which part of the image is visible when cropped.', + 'airo-wp' + )} + /> + )} + + )} +
    + ); +} diff --git a/src/blocks/card/components/inspector/LayoutSettingsPanel.js b/src/blocks/card/components/inspector/LayoutSettingsPanel.js new file mode 100644 index 0000000..b626ae1 --- /dev/null +++ b/src/blocks/card/components/inspector/LayoutSettingsPanel.js @@ -0,0 +1,121 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody, SelectControl, RangeControl } from '@wordpress/components'; + +/** + * Layout Settings Panel Component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to set attributes + * @return {Element} Layout settings panel + */ +export default function LayoutSettingsPanel({ attributes, setAttributes }) { + const { layoutPreset, overlayOpacity, contentAlignment, visualStyle } = + attributes; + + const layoutOptions = [ + { + label: __('Standard (Image Top)', 'airo-wp'), + value: 'standard', + }, + { + label: __('Horizontal (Image Left)', 'airo-wp'), + value: 'horizontal-left', + }, + { + label: __('Horizontal (Image Right)', 'airo-wp'), + value: 'horizontal-right', + }, + { + label: __('Background (Image Behind)', 'airo-wp'), + value: 'background', + }, + { + label: __('Minimal (No Image)', 'airo-wp'), + value: 'minimal', + }, + { + label: __('Featured (Large Image)', 'airo-wp'), + value: 'featured', + }, + ]; + + const alignmentOptions = [ + { label: __('Left', 'airo-wp'), value: 'left' }, + { label: __('Center', 'airo-wp'), value: 'center' }, + { label: __('Right', 'airo-wp'), value: 'right' }, + ]; + + const visualStyleOptions = [ + { label: __('Default', 'airo-wp'), value: 'default' }, + { label: __('Outlined', 'airo-wp'), value: 'outlined' }, + { label: __('Filled', 'airo-wp'), value: 'filled' }, + { label: __('Shadow', 'airo-wp'), value: 'shadow' }, + { label: __('Minimal', 'airo-wp'), value: 'minimal' }, + ]; + + return ( + + setAttributes({ layoutPreset: value })} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + setAttributes({ visualStyle: value })} + help={__( + 'Choose a visual style for the card appearance.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + {layoutPreset === 'background' && ( + <> + + setAttributes({ overlayOpacity: value }) + } + min={0} + max={100} + step={5} + help={__( + 'Darkens the background image to improve text readability.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + setAttributes({ contentAlignment: value }) + } + help={__( + 'Horizontal alignment for content over background image.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + ); +} diff --git a/src/blocks/card/edit.js b/src/blocks/card/edit.js new file mode 100644 index 0000000..975b6a0 --- /dev/null +++ b/src/blocks/card/edit.js @@ -0,0 +1,1034 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + RichText, + InspectorControls, + MediaUpload, + MediaUploadCheck, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, +} from '@wordpress/block-editor'; +import { + SelectControl, + RangeControl, + TextControl, + ToggleControl, + Button, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { useBlockColors } from '../../hooks'; + +/** + * Edit component for Card block + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to set attributes + * @param {string} props.clientId - Block client ID + * @return {Element} Edit component + */ +export default function CardEdit({ attributes, setAttributes, clientId }) { + const { + layoutPreset, + imageUrl, + imageAlt, + imageAspectRatio, + imageCustomAspectRatio, + imageObjectFit, + imageFocalPoint, + badgeText, + badgeStyle, + badgeFloatingPosition, + badgeInlinePosition, + badgeBackgroundColor, + badgeTextColor, + title, + subtitle, + bodyText, + overlayOpacity, + overlayColor, + contentAlignment, + visualStyle, + borderColor, + showImage, + showTitle, + showSubtitle, + showBody, + showBadge, + showCta, + } = attributes; + + // Border panel — migrated to useBlockColors hook. + // colorGradientSettings is returned by the hook (same shape as + // useMultipleOriginColorsAndGradients) and used by the inline panels below. + const { settings: borderColorSettings, colorGradientSettings } = + useBlockColors({ + attributes, + setAttributes, + entries: [ + { + label: __('Border Color', 'airo-wp'), + attribute: 'borderColor', + }, + ], + }); + + // Build block props with border color + const blockStyles = {}; + // Only apply custom border color on styles that have borders (not minimal) + if (borderColor && visualStyle !== 'minimal') { + blockStyles.borderColor = borderColor; + // Ensure border exists + blockStyles.borderWidth = visualStyle === 'outlined' ? '2px' : '1px'; + blockStyles.borderStyle = 'solid'; + } + + const blockProps = useBlockProps({ + className: `airo-wp-card airo-wp-card--${layoutPreset} airo-wp-card--style-${visualStyle}`, + style: blockStyles, + }); + + // Inner blocks props for CTA area + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-card__cta', + }, + { + template: [ + [ + 'airo-wp/icon-button', + { text: __('Learn More', 'airo-wp') }, + ], + ], + templateLock: false, + allowedBlocks: ['airo-wp/icon-button'], + } + ); + + // Calculate image styles + const imageStyles = {}; + if (imageAspectRatio !== 'original') { + if (imageAspectRatio === 'custom' && imageCustomAspectRatio) { + imageStyles.aspectRatio = imageCustomAspectRatio; + } else if (imageAspectRatio === '16-9') { + imageStyles.aspectRatio = '16 / 9'; + } else if (imageAspectRatio === '4-3') { + imageStyles.aspectRatio = '4 / 3'; + } else if (imageAspectRatio === '1-1') { + imageStyles.aspectRatio = '1 / 1'; + } + } + if (imageObjectFit) { + imageStyles.objectFit = imageObjectFit; + } + if (imageObjectFit === 'cover' && imageFocalPoint) { + imageStyles.objectPosition = `${Number(imageFocalPoint.x) * 100}% ${Number(imageFocalPoint.y) * 100}%`; + } + + // Calculate badge styles + const badgeStyles = {}; + if (badgeBackgroundColor) { + badgeStyles.backgroundColor = badgeBackgroundColor; + } + if (badgeTextColor) { + badgeStyles.color = badgeTextColor; + } + + // Calculate overlay styles for background layout + const overlayStyles = {}; + if (layoutPreset === 'background') { + if (overlayColor) { + overlayStyles.backgroundColor = overlayColor; + overlayStyles.opacity = overlayOpacity / 100; + } else { + // Use theme contrast color at full opacity, let overlayOpacity control transparency + overlayStyles.backgroundColor = + 'var(--wp--preset--color--contrast, #000)'; + overlayStyles.opacity = overlayOpacity / 100; + } + } + + // Content alignment class + const contentAlignmentClass = `airo-wp-card__content--${contentAlignment}`; + + // Options for select controls + const layoutOptions = [ + { label: __('Standard (Image Top)', 'airo-wp'), value: 'standard' }, + { + label: __('Horizontal (Image Left)', 'airo-wp'), + value: 'horizontal-left', + }, + { + label: __('Horizontal (Image Right)', 'airo-wp'), + value: 'horizontal-right', + }, + { + label: __('Background (Image Behind)', 'airo-wp'), + value: 'background', + }, + { label: __('Minimal (No Image)', 'airo-wp'), value: 'minimal' }, + { + label: __('Featured (Large Image)', 'airo-wp'), + value: 'featured', + }, + ]; + + const visualStyleOptions = [ + { label: __('Default', 'airo-wp'), value: 'default' }, + { label: __('Outlined', 'airo-wp'), value: 'outlined' }, + { label: __('Filled', 'airo-wp'), value: 'filled' }, + { label: __('Shadow', 'airo-wp'), value: 'shadow' }, + { label: __('Minimal', 'airo-wp'), value: 'minimal' }, + ]; + + const alignmentOptions = [ + { label: __('Left', 'airo-wp'), value: 'left' }, + { label: __('Center', 'airo-wp'), value: 'center' }, + { label: __('Right', 'airo-wp'), value: 'right' }, + ]; + + const badgeStyleOptions = [ + { label: __('Floating (Over Card)', 'airo-wp'), value: 'floating' }, + { label: __('Inline (In Content)', 'airo-wp'), value: 'inline' }, + ]; + + const badgeFloatingPositionOptions = [ + { label: __('Top Left', 'airo-wp'), value: 'top-left' }, + { label: __('Top Right', 'airo-wp'), value: 'top-right' }, + { label: __('Bottom Left', 'airo-wp'), value: 'bottom-left' }, + { label: __('Bottom Right', 'airo-wp'), value: 'bottom-right' }, + ]; + + const badgeInlinePositionOptions = [ + { label: __('Above Title', 'airo-wp'), value: 'above-title' }, + { label: __('Below Title', 'airo-wp'), value: 'below-title' }, + ]; + + // Render badge + const renderBadge = () => { + if (!showBadge || !badgeText) { + return null; + } + + const badgeClass = + badgeStyle === 'floating' + ? `airo-wp-card__badge airo-wp-card__badge--floating airo-wp-card__badge--${badgeFloatingPosition}` + : `airo-wp-card__badge airo-wp-card__badge--inline airo-wp-card__badge--${badgeInlinePosition}`; + + return ( + + {badgeText} + + ); + }; + + // Render image + const renderImage = () => { + if (!showImage || layoutPreset === 'minimal') { + return null; + } + + // Placeholder for background layout + if (layoutPreset === 'background') { + if (!imageUrl) { + return ( +
    +
    + + + {__('Select background image', 'airo-wp')} + +
    +
    + ); + } + return ( +
    +
    +
    + ); + } + + // Placeholder for standard layouts + if (!imageUrl) { + return ( +
    +
    + + {__('Select image', 'airo-wp')} +
    +
    + ); + } + + return ( +
    + {imageAlt} +
    + ); + }; + + // Render content + const renderContent = () => ( +
    + {badgeStyle === 'inline' && + badgeInlinePosition === 'above-title' && + renderBadge()} + + {showTitle && ( + setAttributes({ title: value })} + placeholder={__('Card Title…', 'airo-wp')} + /> + )} + + {badgeStyle === 'inline' && + badgeInlinePosition === 'below-title' && + renderBadge()} + + {showSubtitle && ( + setAttributes({ subtitle: value })} + placeholder={__('Card Subtitle…', 'airo-wp')} + /> + )} + + {showBody && ( + setAttributes({ bodyText: value })} + placeholder={__( + 'Card description goes here…', + 'airo-wp' + )} + /> + )} + + {showCta &&
    } +
    + ); + + return ( + <> + + + setAttributes({ + layoutPreset: 'standard', + visualStyle: 'default', + imageId: 0, + imageUrl: '', + imageAlt: '', + imageAspectRatio: '16-9', + imageCustomAspectRatio: '', + imageObjectFit: 'cover', + imageFocalPoint: { x: 0.5, y: 0.5 }, + overlayOpacity: 80, + contentAlignment: 'center', + badgeText: '', + badgeStyle: 'floating', + badgeFloatingPosition: 'top-right', + badgeInlinePosition: 'above-title', + showImage: true, + showTitle: true, + showSubtitle: true, + showBody: true, + showBadge: true, + showCta: true, + }) + } + > + layoutPreset !== 'standard'} + onDeselect={() => + setAttributes({ layoutPreset: 'standard' }) + } + isShownByDefault + > + + setAttributes({ layoutPreset: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + visualStyle !== 'default'} + onDeselect={() => + setAttributes({ visualStyle: 'default' }) + } + isShownByDefault + > + + setAttributes({ visualStyle: value }) + } + help={__( + 'Choose a visual style for the card appearance.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {layoutPreset !== 'minimal' && showImage && ( + !!imageUrl} + onDeselect={() => + setAttributes({ + imageId: 0, + imageUrl: '', + imageAlt: '', + imageFocalPoint: { x: 0.5, y: 0.5 }, + }) + } + isShownByDefault + > + + { + setAttributes({ + imageId: media.id, + imageUrl: media.url, + imageAlt: media.alt || '', + }); + }} + allowedTypes={['image']} + value={imageUrl} + render={({ open }) => ( + <> + {imageUrl ? ( + <> + {imageAlt} +
    + + +
    + + ) : ( + + )} + + )} + /> +
    +
    + )} + + {layoutPreset !== 'minimal' && showImage && imageUrl && ( + imageAlt !== ''} + onDeselect={() => setAttributes({ imageAlt: '' })} + isShownByDefault + > + + setAttributes({ imageAlt: value }) + } + help={__( + 'Describe the image for accessibility.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {layoutPreset !== 'minimal' && showImage && imageUrl && ( + imageAspectRatio !== '16-9'} + onDeselect={() => + setAttributes({ + imageAspectRatio: '16-9', + imageCustomAspectRatio: '', + }) + } + isShownByDefault + > + + setAttributes({ + imageAspectRatio: value, + }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {layoutPreset !== 'minimal' && + showImage && + imageUrl && + imageAspectRatio === 'custom' && ( + imageCustomAspectRatio !== ''} + onDeselect={() => + setAttributes({ + imageCustomAspectRatio: '', + }) + } + isShownByDefault + > + + setAttributes({ + imageCustomAspectRatio: value, + }) + } + placeholder="16 / 9" + help={__( + 'E.g., "16 / 9" or "2 / 1"', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {layoutPreset !== 'minimal' && showImage && imageUrl && ( + imageObjectFit !== 'cover'} + onDeselect={() => + setAttributes({ imageObjectFit: 'cover' }) + } + isShownByDefault + > + + setAttributes({ + imageObjectFit: value, + }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {layoutPreset === 'background' && ( + overlayOpacity !== 80} + onDeselect={() => + setAttributes({ overlayOpacity: 80 }) + } + isShownByDefault + > + + setAttributes({ overlayOpacity: value }) + } + min={0} + max={100} + step={5} + help={__( + 'Darkens the background image to improve text readability.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {layoutPreset === 'background' && ( + contentAlignment !== 'center'} + onDeselect={() => + setAttributes({ contentAlignment: 'center' }) + } + isShownByDefault + > + + setAttributes({ contentAlignment: value }) + } + help={__( + 'Horizontal alignment for content over background image.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + badgeText !== ''} + onDeselect={() => setAttributes({ badgeText: '' })} + isShownByDefault + > + + setAttributes({ badgeText: value }) + } + placeholder={__('NEW', 'airo-wp')} + help={__( + 'Leave empty to hide the badge.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {badgeText && ( + badgeStyle !== 'floating'} + onDeselect={() => + setAttributes({ badgeStyle: 'floating' }) + } + isShownByDefault + > + + setAttributes({ badgeStyle: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {badgeText && badgeStyle === 'floating' && ( + + badgeFloatingPosition !== 'top-right' + } + onDeselect={() => + setAttributes({ + badgeFloatingPosition: 'top-right', + }) + } + isShownByDefault + > + + setAttributes({ + badgeFloatingPosition: value, + }) + } + help={__( + 'Position the badge over the card.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {badgeText && badgeStyle === 'inline' && ( + + badgeInlinePosition !== 'above-title' + } + onDeselect={() => + setAttributes({ + badgeInlinePosition: 'above-title', + }) + } + isShownByDefault + > + + setAttributes({ + badgeInlinePosition: value, + }) + } + help={__( + 'Position the badge in the content flow.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {layoutPreset !== 'minimal' && ( + showImage !== true} + onDeselect={() => + setAttributes({ showImage: true }) + } + isShownByDefault + > + + setAttributes({ showImage: value }) + } + help={__( + 'Display the card image.', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + )} + + showTitle !== true} + onDeselect={() => setAttributes({ showTitle: true })} + isShownByDefault + > + + setAttributes({ showTitle: value }) + } + help={__('Display the card title.', 'airo-wp')} + __nextHasNoMarginBottom + /> + + + showSubtitle !== true} + onDeselect={() => setAttributes({ showSubtitle: true })} + isShownByDefault + > + + setAttributes({ showSubtitle: value }) + } + help={__( + 'Display the card subtitle.', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + showBody !== true} + onDeselect={() => setAttributes({ showBody: true })} + isShownByDefault + > + + setAttributes({ showBody: value }) + } + help={__( + 'Display the card body text.', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + showBadge !== true} + onDeselect={() => setAttributes({ showBadge: true })} + isShownByDefault + > + + setAttributes({ showBadge: value }) + } + help={__( + 'Display the badge element.', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + showCta !== true} + onDeselect={() => setAttributes({ showCta: true })} + isShownByDefault + > + + setAttributes({ showCta: value }) + } + help={__( + 'Display the call-to-action button.', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + +
    +
    + + + + + + setAttributes({ + badgeBackgroundColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Badge Text', 'airo-wp'), + colorValue: decodeColorValue( + badgeTextColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + badgeTextColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + {layoutPreset === 'background' && ( + + setAttributes({ + overlayColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + )} + + +
    + {badgeStyle === 'floating' && renderBadge()} + {layoutPreset === 'background' && renderImage()} + +
    + {layoutPreset !== 'background' && renderImage()} + {renderContent()} +
    +
    + + ); +} diff --git a/src/blocks/card/editor.scss b/src/blocks/card/editor.scss new file mode 100644 index 0000000..2913fc4 --- /dev/null +++ b/src/blocks/card/editor.scss @@ -0,0 +1,97 @@ +/** + * Card Block Editor Styles + * Editor-specific visual hints and overrides + */ + +.airo-wp-card { + // Ensure minimum height in editor for empty cards + &.airo-wp-card--standard, + &.airo-wp-card--horizontal-left, + &.airo-wp-card--horizontal-right, + &.airo-wp-card--minimal, + &.airo-wp-card--featured { + min-height: 150px; + } + + &.airo-wp-card--background { + // Already has min-height in style.scss + } + + // Image placeholder styling + .airo-wp-card__image-wrapper--placeholder, + .airo-wp-card__background--placeholder { + min-height: 200px; + background: var(--wp--preset--color--contrast-2, #f0f0f0); + display: flex; + align-items: center; + justify-content: center; + } + + .airo-wp-card__background--placeholder { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + } + + .airo-wp-card__placeholder-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + color: var(--wp--preset--color--contrast-3, #999); + font-size: 0.875rem; + + .dashicons { + font-size: 3rem; + width: 3rem; + height: 3rem; + opacity: 0.5; + } + } + + // RichText placeholders styling + .block-editor-rich-text__editable { + + &[data-is-placeholder-visible="true"] { + opacity: 0.6; + } + } + + // Make RichText areas visible in editor + .airo-wp-card__title, + .airo-wp-card__subtitle, + .airo-wp-card__body { + + &:focus { + outline: 1px dashed var(--wp--preset--color--contrast-3, #ccc); + outline-offset: 2px; + } + } + + // CTA area hint + .airo-wp-card__cta { + + &:empty { + min-height: 40px; + border: 1px dashed var(--wp--preset--color--contrast-3, #ccc); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + + &::before { + content: 'Add CTA button'; + color: var(--wp--preset--color--contrast-3, #999); + font-size: 0.75rem; + } + } + } + + // Image upload control styling + .airo-wp-card-image-upload { + margin-bottom: 1rem; + } +} diff --git a/src/blocks/card/index.js b/src/blocks/card/index.js new file mode 100644 index 0000000..a4c0e28 --- /dev/null +++ b/src/blocks/card/index.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import Edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; +import './style.scss'; +import './editor.scss'; + +/** + * Register: Card Block + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/ + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + + ), + foreground: ICON_COLOR, + }, + edit: Edit, + save, +}); diff --git a/src/blocks/card/save.js b/src/blocks/card/save.js new file mode 100644 index 0000000..d0dddc3 --- /dev/null +++ b/src/blocks/card/save.js @@ -0,0 +1,236 @@ +/** + * WordPress dependencies + */ +import { + useBlockProps, + useInnerBlocksProps, + RichText, +} from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { isValidImageUrl } from '../../utils/is-valid-image-url'; + +/** + * Save component for Card block + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @return {Element} Save component + */ +export default function CardSave({ attributes }) { + const { + layoutPreset, + imageUrl, + imageAlt, + imageAspectRatio, + imageCustomAspectRatio, + imageObjectFit, + imageFocalPoint, + badgeText, + badgeStyle, + badgeFloatingPosition, + badgeInlinePosition, + badgeBackgroundColor, + badgeTextColor, + title, + subtitle, + bodyText, + overlayOpacity, + overlayColor, + contentAlignment, + visualStyle, + borderColor, + showImage, + showTitle, + showSubtitle, + showBody, + showBadge, + showCta, + } = attributes; + + // Build block props with border color + const blockStyles = {}; + // Only apply custom border color on styles that have borders (not minimal) + if (borderColor && visualStyle !== 'minimal') { + blockStyles.borderColor = borderColor; + // Ensure border exists + blockStyles.borderWidth = visualStyle === 'outlined' ? '2px' : '1px'; + blockStyles.borderStyle = 'solid'; + } + + const blockProps = useBlockProps.save({ + className: `airo-wp-card airo-wp-card--${layoutPreset} airo-wp-card--style-${visualStyle}`, + style: blockStyles, + }); + + // Inner blocks props for CTA area + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-card__cta', + }); + + // Calculate image styles + const imageStyles = {}; + if (imageAspectRatio !== 'original') { + if (imageAspectRatio === 'custom' && imageCustomAspectRatio) { + imageStyles.aspectRatio = imageCustomAspectRatio; + } else if (imageAspectRatio === '16-9') { + imageStyles.aspectRatio = '16 / 9'; + } else if (imageAspectRatio === '4-3') { + imageStyles.aspectRatio = '4 / 3'; + } else if (imageAspectRatio === '1-1') { + imageStyles.aspectRatio = '1 / 1'; + } + } + if (imageObjectFit) { + imageStyles.objectFit = imageObjectFit; + } + if (imageObjectFit === 'cover' && imageFocalPoint) { + imageStyles.objectPosition = `${Number(imageFocalPoint.x) * 100}% ${Number(imageFocalPoint.y) * 100}%`; + } + + // Calculate badge styles + const badgeStyles = {}; + if (badgeBackgroundColor) { + badgeStyles.backgroundColor = badgeBackgroundColor; + } + if (badgeTextColor) { + badgeStyles.color = badgeTextColor; + } + + // Calculate overlay styles for background layout + const overlayStyles = {}; + if (layoutPreset === 'background') { + if (overlayColor) { + overlayStyles.backgroundColor = overlayColor; + overlayStyles.opacity = overlayOpacity / 100; + } else { + // Use theme contrast color at full opacity, let overlayOpacity control transparency + overlayStyles.backgroundColor = + 'var(--wp--preset--color--contrast, #000)'; + overlayStyles.opacity = overlayOpacity / 100; + } + } + + // Content alignment class + const contentAlignmentClass = `airo-wp-card__content--${contentAlignment}`; + + // Render badge + const renderBadge = () => { + if (!showBadge || !badgeText) { + return null; + } + + const badgeClass = + badgeStyle === 'floating' + ? `airo-wp-card__badge airo-wp-card__badge--floating airo-wp-card__badge--${badgeFloatingPosition}` + : `airo-wp-card__badge airo-wp-card__badge--inline airo-wp-card__badge--${badgeInlinePosition}`; + + return ( + + {badgeText} + + ); + }; + + // Render image + const renderImage = () => { + if ( + !showImage || + layoutPreset === 'minimal' || + !imageUrl || + !isValidImageUrl(imageUrl) + ) { + return null; + } + + if (layoutPreset === 'background') { + return ( +
    +
    +
    + ); + } + + // Provide fallback alt text for accessibility + const altText = imageAlt || __('Card image', 'airo-wp'); + const imageProps = { + src: imageUrl, + alt: altText, + className: 'airo-wp-card__image', + style: imageStyles, + loading: 'lazy', + }; + + // Hide decorative images from screen readers + if (!imageAlt) { + imageProps['aria-hidden'] = 'true'; + } + + return ( +
    + {/* eslint-disable-next-line jsx-a11y/alt-text */} + +
    + ); + }; + + // Render content + const renderContent = () => ( +
    + {badgeStyle === 'inline' && + badgeInlinePosition === 'above-title' && + renderBadge()} + + {showTitle && title && ( + + )} + + {badgeStyle === 'inline' && + badgeInlinePosition === 'below-title' && + renderBadge()} + + {showSubtitle && subtitle && ( + + )} + + {showBody && bodyText && ( + + )} + + {showCta &&
    } +
    + ); + + return ( +
    + {badgeStyle === 'floating' && renderBadge()} + {layoutPreset === 'background' && renderImage()} + +
    + {layoutPreset !== 'background' && renderImage()} + {renderContent()} +
    +
    + ); +} diff --git a/src/blocks/card/style.scss b/src/blocks/card/style.scss new file mode 100644 index 0000000..c87bc43 --- /dev/null +++ b/src/blocks/card/style.scss @@ -0,0 +1,427 @@ +/** + * Card Block Styles + * Supports 6 layout presets: standard, horizontal-left, horizontal-right, background, minimal, featured + */ + +.airo-wp-card { + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + + // CRITICAL: Ensure borders and padding are included in the total width + // Prevents cards from overflowing grid cells when borders are applied + box-sizing: border-box; + + // Default background when no inline styles + &:not([style*="background"]) { + background-color: var(--wp--preset--color--base, #fff); + } + + // Nested container handling - cards inside other containers should be full width + :where(.airo-wp-stack__inner, .airo-wp-flex__inner, .airo-wp-grid__inner) > & { + width: 100% !important; + } +} + +// CRITICAL: Cards inside grid need additional constraints to prevent overlap +// Note: min-width: 0 is already provided by grid block styles (.airo-wp-grid__inner > *) +.airo-wp-grid__inner > .airo-wp-card { + // Constrain max width to prevent overflow when borders are applied + max-width: 100%; + // Fill the full height of the grid cell for consistent row heights + // Safe because .airo-wp-card has overflow: hidden to handle content overflow + height: 100%; +} + +// Inner wrapper +.airo-wp-card__inner { + display: flex; + flex-direction: column; + flex: 1; + position: relative; + z-index: 1; +} + +// Image wrapper and image +.airo-wp-card__image-wrapper { + position: relative; + overflow: hidden; + width: 100%; +} + +.airo-wp-card__image { + display: block; + width: 100%; + height: auto; +} + +// Background image layout +.airo-wp-card__background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + z-index: 0; +} + +.airo-wp-card__overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +// Content wrapper +.airo-wp-card__content { + display: flex; + flex-direction: column; + gap: 1rem; + flex: 1; +} + +// Content elements +.airo-wp-card__title { + margin: 0; + font-size: var(--wp--preset--font-size--large, 1.75rem); + font-weight: 600; + line-height: 1.2; +} + +.airo-wp-card__subtitle { + margin: 0; + font-size: var(--wp--preset--font-size--medium, 1.125rem); + opacity: 0.8; +} + +.airo-wp-card__body { + margin: 0; + font-size: var(--wp--preset--font-size--medium, 1rem); + line-height: 1.6; +} + +.airo-wp-card__cta { + margin-top: auto; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; + + // Reduce spacing in background layout + .airo-wp-card--background & { + margin-top: 1rem; + } +} + +// Badge styles +.airo-wp-card__badge { + display: inline-block; + padding: 0.25rem 0.75rem; + font-size: var(--wp--preset--font-size--small, 0.875rem); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + border-radius: var(--wp--custom--spacing--small, 0.25rem); + + // Default badge colors when no inline styles + &:not([style*="background"]) { + background-color: var(--wp--preset--color--contrast, #000); + } + + &:not([style*="color"]) { + color: var(--wp--preset--color--base, #fff); + } +} + +// Floating badge +.airo-wp-card__badge--floating { + position: absolute; + z-index: 10; + + &.airo-wp-card__badge--top-left { + top: 1rem; + left: 1rem; + } + + &.airo-wp-card__badge--top-right { + top: 1rem; + right: 1rem; + } + + &.airo-wp-card__badge--bottom-left { + bottom: 1rem; + left: 1rem; + } + + &.airo-wp-card__badge--bottom-right { + bottom: 1rem; + right: 1rem; + } +} + +// Inline badge +.airo-wp-card__badge--inline { + width: fit-content; +} + +// ======================================== +// Visual Styles +// ======================================== + +// Default style - subtle border +.airo-wp-card--style-default { + border: 1px solid var(--wp--preset--color--contrast-2, #e5e5e5); +} + +// Outlined style +.airo-wp-card--style-outlined { + border: 2px solid var(--wp--preset--color--contrast-2, #e0e0e0); + background: transparent; +} + +// Filled style +.airo-wp-card--style-filled { + background-color: var(--wp--preset--color--accent-4, var(--wp--preset--color--accent-5, #e8f4f8)) !important; +} + +// Shadow style +.airo-wp-card--style-shadow { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: 1px solid var(--wp--preset--color--contrast-2, #e0e0e0); +} + +// Minimal style (clean, no decorations) +.airo-wp-card--style-minimal { + border: none; + box-shadow: none; +} + +// ======================================== +// Layout Preset: Standard (Image Top) +// ======================================== +.airo-wp-card--standard { + + .airo-wp-card__inner { + flex-direction: column; + } + + .airo-wp-card__content { + padding: 1.5rem; + } +} + +// ======================================== +// Layout Preset: Horizontal Left +// ======================================== +.airo-wp-card--horizontal-left { + + .airo-wp-card__inner { + flex-direction: row; + align-items: stretch; + } + + .airo-wp-card__image-wrapper { + flex: 0 0 50%; + max-width: 50%; + } + + .airo-wp-card__content { + flex: 1; + padding: 1.5rem; + } + + // Stack on mobile + @media (max-width: 768px) { + + .airo-wp-card__inner { + flex-direction: column; + } + + .airo-wp-card__image-wrapper { + flex: 1; + max-width: 100%; + } + } +} + +// ======================================== +// Layout Preset: Horizontal Right +// ======================================== +.airo-wp-card--horizontal-right { + + .airo-wp-card__inner { + flex-direction: row-reverse; + align-items: stretch; + } + + .airo-wp-card__image-wrapper { + flex: 0 0 50%; + max-width: 50%; + } + + .airo-wp-card__content { + flex: 1; + padding: 1.5rem; + } + + // Stack on mobile + @media (max-width: 768px) { + + .airo-wp-card__inner { + flex-direction: column; + } + + .airo-wp-card__image-wrapper { + flex: 1; + max-width: 100%; + } + } +} + +// ======================================== +// Layout Preset: Background +// ======================================== +.airo-wp-card--background { + min-height: 400px; + + .airo-wp-card__inner { + justify-content: center; + align-items: stretch; + padding: 0; + } + + .airo-wp-card__content { + position: relative; + z-index: 2; + color: var(--wp--preset--color--base, #fff); + width: 100%; + max-width: 100%; + padding: 2rem; + justify-content: center; + + &.airo-wp-card__content--left { + align-items: flex-start; + text-align: left; + } + + &.airo-wp-card__content--center { + align-items: center; + text-align: center; + } + + &.airo-wp-card__content--right { + align-items: flex-end; + text-align: right; + } + } + + .airo-wp-card__title, + .airo-wp-card__subtitle, + .airo-wp-card__body { + color: inherit; + } + + // Reduce min-height on mobile + @media (max-width: 768px) { + min-height: 300px; + + .airo-wp-card__inner { + padding: 1.5rem; + } + } +} + +// ======================================== +// Layout Preset: Minimal (No Image) +// ======================================== +.airo-wp-card--minimal { + + .airo-wp-card__inner { + padding: 2rem; + } + + .airo-wp-card__content { + text-align: center; + align-items: center; + } + + @media (max-width: 768px) { + + .airo-wp-card__inner { + padding: 1.5rem; + } + } +} + +// ======================================== +// Layout Preset: Featured (Large Image) +// ======================================== +.airo-wp-card--featured { + + .airo-wp-card__inner { + flex-direction: column; + } + + .airo-wp-card__image-wrapper { + + .airo-wp-card__image { + aspect-ratio: 2 / 1; + object-fit: cover; + } + } + + .airo-wp-card__content { + padding: 1.5rem; + } + + .airo-wp-card__title { + font-size: var(--wp--preset--font-size--x-large, 2.25rem); + } +} + +// ======================================== +// Responsive adjustments +// ======================================== +@media (max-width: 480px) { + + .airo-wp-card__title { + font-size: var(--wp--preset--font-size--medium, 1.5rem); + } + + .airo-wp-card__subtitle { + font-size: var(--wp--preset--font-size--small, 1rem); + } + + .airo-wp-card__badge { + font-size: 0.75rem; + padding: 0.2rem 0.6rem; + } + + .airo-wp-card__badge--floating { + + &.airo-wp-card__badge--top-left, + &.airo-wp-card__badge--top-right { + top: 0.75rem; + } + + &.airo-wp-card__badge--top-left, + &.airo-wp-card__badge--bottom-left { + left: 0.75rem; + } + + &.airo-wp-card__badge--top-right, + &.airo-wp-card__badge--bottom-right { + right: 0.75rem; + } + + &.airo-wp-card__badge--bottom-left, + &.airo-wp-card__badge--bottom-right { + bottom: 0.75rem; + } + } +} diff --git a/src/blocks/comparison-table/block.json b/src/blocks/comparison-table/block.json new file mode 100644 index 0000000..46e138e --- /dev/null +++ b/src/blocks/comparison-table/block.json @@ -0,0 +1,281 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/comparison-table", + "version": "1.0.0", + "title": "Comparison Table", + "category": "airo-wp", + "description": "Display a feature comparison table for products, services, or plans", + "keywords": [ + "comparison", + "table", + "pricing", + "features", + "plans" + ], + "textdomain": "airo-wp", + "icon": "editor-table", + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "inserter": true, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "padding": true + } + }, + "color": { + "background": true, + "text": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true + } + } + }, + "attributes": { + "columns": { + "type": "array", + "default": [ + { + "name": "Basic", + "link": "", + "linkText": "Get Started", + "featured": false + }, + { + "name": "Pro", + "link": "", + "linkText": "Get Started", + "featured": true + }, + { + "name": "Enterprise", + "link": "", + "linkText": "Contact Us", + "featured": false + } + ] + }, + "rows": { + "type": "array", + "default": [ + { + "label": "Storage", + "tooltip": "", + "cells": [ + { + "type": "text", + "value": "5 GB" + }, + { + "type": "text", + "value": "50 GB" + }, + { + "type": "text", + "value": "Unlimited" + } + ] + }, + { + "label": "Users", + "tooltip": "", + "cells": [ + { + "type": "text", + "value": "1" + }, + { + "type": "text", + "value": "10" + }, + { + "type": "text", + "value": "Unlimited" + } + ] + }, + { + "label": "Priority Support", + "tooltip": "Get faster response times from our team", + "cells": [ + { + "type": "cross", + "value": "" + }, + { + "type": "check", + "value": "" + }, + { + "type": "check", + "value": "" + } + ] + }, + { + "label": "API Access", + "tooltip": "", + "cells": [ + { + "type": "cross", + "value": "" + }, + { + "type": "check", + "value": "" + }, + { + "type": "check", + "value": "" + } + ] + } + ] + }, + "alternatingRows": { + "type": "boolean", + "default": true + }, + "responsiveMode": { + "type": "string", + "enum": [ + "scroll", + "stack" + ], + "default": "scroll" + }, + "featuredColumnColor": { + "type": "string", + "default": "" + }, + "headerBackgroundColor": { + "type": "string", + "default": "" + }, + "headerTextColor": { + "type": "string", + "default": "" + }, + "showCtaButtons": { + "type": "boolean", + "default": true + }, + "ctaStyle": { + "type": "string", + "enum": [ + "filled", + "outlined" + ], + "default": "filled" + } + }, + "example": { + "attributes": { + "columns": [ + { + "name": "Starter", + "link": "#", + "linkText": "Choose", + "featured": false + }, + { + "name": "Professional", + "link": "#", + "linkText": "Choose", + "featured": true + }, + { + "name": "Business", + "link": "#", + "linkText": "Choose", + "featured": false + } + ], + "rows": [ + { + "label": "Projects", + "tooltip": "", + "cells": [ + { + "type": "text", + "value": "3" + }, + { + "type": "text", + "value": "Unlimited" + }, + { + "type": "text", + "value": "Unlimited" + } + ] + }, + { + "label": "Team Members", + "tooltip": "", + "cells": [ + { + "type": "text", + "value": "1" + }, + { + "type": "text", + "value": "10" + }, + { + "type": "text", + "value": "Unlimited" + } + ] + }, + { + "label": "Priority Support", + "tooltip": "", + "cells": [ + { + "type": "cross", + "value": "" + }, + { + "type": "check", + "value": "" + }, + { + "type": "check", + "value": "" + } + ] + } + ] + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "viewScript": "file:./view.js" +} diff --git a/src/blocks/comparison-table/edit.js b/src/blocks/comparison-table/edit.js new file mode 100644 index 0000000..78869f2 --- /dev/null +++ b/src/blocks/comparison-table/edit.js @@ -0,0 +1,972 @@ +/** + * Comparison Table Block - Edit Component + * + * Provides a table editor with inline editing for column headers, + * row labels, and cell content. Supports multiple cell types + * (text, check, cross) and column management. + */ + +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + InspectorControls, + RichText, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { + ToggleControl, + SelectControl, + Button, + TextControl, + Tooltip, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useState } from '@wordpress/element'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const DEFAULT_COLUMNS = [ + { name: 'Basic', link: '', linkText: 'Get Started', featured: false }, + { name: 'Pro', link: '', linkText: 'Get Started', featured: true }, + { name: 'Enterprise', link: '', linkText: 'Contact Us', featured: false }, +]; +const DEFAULT_ROWS = [ + { + label: 'Storage', + tooltip: '', + cells: [ + { type: 'text', value: '5 GB' }, + { type: 'text', value: '50 GB' }, + { type: 'text', value: 'Unlimited' }, + ], + }, + { + label: 'Users', + tooltip: '', + cells: [ + { type: 'text', value: '1' }, + { type: 'text', value: '10' }, + { type: 'text', value: 'Unlimited' }, + ], + }, + { + label: 'Priority Support', + tooltip: 'Get faster response times from our team', + cells: [ + { type: 'cross', value: '' }, + { type: 'check', value: '' }, + { type: 'check', value: '' }, + ], + }, + { + label: 'API Access', + tooltip: '', + cells: [ + { type: 'cross', value: '' }, + { type: 'check', value: '' }, + { type: 'check', value: '' }, + ], + }, +]; + +/** + * Check icon SVG for cell display + * + * @return {JSX.Element} Check icon + */ +const CheckIcon = () => ( + + + +); + +/** + * Cross icon SVG for cell display + * + * @return {JSX.Element} Cross icon + */ +const CrossIcon = () => ( + + + + +); + +/** + * Renders the content of a single table cell based on its type + * + * @param {Object} props - Component props + * @param {Object} props.cell - Cell data object + * @param {number} props.rowIndex - Row index + * @param {number} props.colIndex - Column index + * @param {Function} props.onCellChange - Callback for cell changes + * @return {JSX.Element} Cell content + */ +function CellContent({ cell, rowIndex, colIndex, onCellChange }) { + if (cell.type === 'check') { + return ; + } + + if (cell.type === 'cross') { + return ; + } + + return ( + onCellChange(rowIndex, colIndex, { value })} + placeholder={__('--', 'airo-wp')} + allowedFormats={['core/bold', 'core/italic']} + /> + ); +} + +/** + * Edit component for the Comparison Table block + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @param {string} props.clientId - Block client ID + * @return {JSX.Element} Comparison Table edit component + */ +export default function ComparisonTableEdit({ + attributes, + setAttributes, + clientId, +}) { + const { + columns, + rows, + alternatingRows, + responsiveMode, + featuredColumnColor, + headerBackgroundColor, + headerTextColor, + showCtaButtons, + ctaStyle, + } = attributes; + + const [selectedCell, setSelectedCell] = useState(null); + + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + /** + * Updates a specific column attribute + * + * @param {number} colIndex - Column index to update + * @param {Object} changes - Object of attribute changes + */ + const updateColumn = (colIndex, changes) => { + const newColumns = columns.map((col, i) => + i === colIndex ? { ...col, ...changes } : col + ); + setAttributes({ columns: newColumns }); + }; + + /** + * Updates a specific row attribute + * + * @param {number} rowIndex - Row index to update + * @param {Object} changes - Object of attribute changes + */ + const updateRow = (rowIndex, changes) => { + const newRows = rows.map((row, i) => + i === rowIndex ? { ...row, ...changes } : row + ); + setAttributes({ rows: newRows }); + }; + + /** + * Updates a specific cell within a row + * + * @param {number} rowIndex - Row index + * @param {number} colIndex - Column index + * @param {Object} changes - Object of attribute changes + */ + const updateCell = (rowIndex, colIndex, changes) => { + const newRows = rows.map((row, rIdx) => { + if (rIdx !== rowIndex) { + return row; + } + const newCells = row.cells.map((cell, cIdx) => + cIdx === colIndex ? { ...cell, ...changes } : cell + ); + return { ...row, cells: newCells }; + }); + setAttributes({ rows: newRows }); + }; + + /** + * Adds a new column to the table + */ + const addColumn = () => { + if (columns.length >= 6) { + return; + } + const newColumns = [ + ...columns, + { + name: __('Plan', 'airo-wp'), + link: '', + linkText: __('Get Started', 'airo-wp'), + featured: false, + }, + ]; + const newRows = rows.map((row) => ({ + ...row, + cells: [...row.cells, { type: 'text', value: '' }], + })); + setAttributes({ columns: newColumns, rows: newRows }); + }; + + /** + * Removes a column from the table + * + * @param {number} colIndex - Column index to remove + */ + const removeColumn = (colIndex) => { + if (columns.length <= 2) { + return; + } + const newColumns = columns.filter((_, i) => i !== colIndex); + const newRows = rows.map((row) => ({ + ...row, + cells: row.cells.filter((_, i) => i !== colIndex), + })); + setAttributes({ columns: newColumns, rows: newRows }); + }; + + /** + * Adds a new row to the table + */ + const addRow = () => { + const newRow = { + label: __('Feature', 'airo-wp'), + tooltip: '', + cells: columns.map(() => ({ type: 'text', value: '' })), + }; + setAttributes({ rows: [...rows, newRow] }); + }; + + /** + * Removes a row from the table + * + * @param {number} rowIndex - Row index to remove + */ + const removeRow = (rowIndex) => { + if (rows.length <= 1) { + return; + } + setAttributes({ rows: rows.filter((_, i) => i !== rowIndex) }); + }; + + /** + * Moves a row up or down in the table + * + * @param {number} rowIndex - Row index to move + * @param {string} direction - 'up' or 'down' + */ + const moveRow = (rowIndex, direction) => { + const newRows = [...rows]; + const targetIndex = direction === 'up' ? rowIndex - 1 : rowIndex + 1; + if (targetIndex < 0 || targetIndex >= rows.length) { + return; + } + [newRows[rowIndex], newRows[targetIndex]] = [ + newRows[targetIndex], + newRows[rowIndex], + ]; + setAttributes({ rows: newRows }); + }; + + const blockProps = useBlockProps({ + className: [ + 'airo-wp-comparison-table', + alternatingRows && 'airo-wp-comparison-table--alternating', + responsiveMode === 'stack' && + 'airo-wp-comparison-table--responsive-stack', + responsiveMode === 'scroll' && + 'airo-wp-comparison-table--responsive-scroll', + ] + .filter(Boolean) + .join(' '), + style: { + ...(featuredColumnColor && { + '--airo-wp-comparison-featured-color': + convertColorToCSSVar(featuredColumnColor), + }), + ...(headerBackgroundColor && { + '--airo-wp-comparison-header-bg': convertColorToCSSVar( + headerBackgroundColor + ), + }), + ...(headerTextColor && { + '--airo-wp-comparison-header-text': + convertColorToCSSVar(headerTextColor), + }), + }, + }); + + return ( + <> + {/* Color Controls */} + + + setAttributes({ + headerBackgroundColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Header Text', 'airo-wp'), + colorValue: decodeColorValue( + headerTextColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + headerTextColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __( + 'Featured Column Highlight', + 'airo-wp' + ), + colorValue: decodeColorValue( + featuredColumnColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + featuredColumnColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + + {/* Table Settings */} + + + setAttributes({ + alternatingRows: true, + responsiveMode: 'scroll', + showCtaButtons: true, + ctaStyle: 'filled', + columns: DEFAULT_COLUMNS, + rows: DEFAULT_ROWS, + }) + } + > + alternatingRows !== true} + onDeselect={() => + setAttributes({ alternatingRows: true }) + } + isShownByDefault + > + + setAttributes({ alternatingRows: value }) + } + __nextHasNoMarginBottom + /> + + + responsiveMode !== 'scroll'} + onDeselect={() => + setAttributes({ responsiveMode: 'scroll' }) + } + isShownByDefault + > + + setAttributes({ responsiveMode: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + showCtaButtons !== true} + onDeselect={() => + setAttributes({ showCtaButtons: true }) + } + isShownByDefault + > + + setAttributes({ showCtaButtons: value }) + } + __nextHasNoMarginBottom + /> + + + {showCtaButtons && ( + ctaStyle !== 'filled'} + onDeselect={() => + setAttributes({ ctaStyle: 'filled' }) + } + isShownByDefault + > + + setAttributes({ ctaStyle: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + + JSON.stringify(columns) !== + JSON.stringify(DEFAULT_COLUMNS) + } + onDeselect={() => + setAttributes({ columns: DEFAULT_COLUMNS }) + } + isShownByDefault + > + {columns.map((col, colIndex) => ( +
    +

    + {col.name || `Column ${colIndex + 1}`} + {columns.length > 2 && ( +

    + + + updateColumn(colIndex, { + featured: value, + }) + } + __nextHasNoMarginBottom + /> + + {showCtaButtons && ( + <> + + updateColumn(colIndex, { + link: value, + }) + } + placeholder="https://" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + updateColumn(colIndex, { + linkText: value, + }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {colIndex < columns.length - 1 && ( +
    + )} +
    + ))} + + {columns.length < 6 && ( + + )} +
    + + + JSON.stringify(rows) !== + JSON.stringify(DEFAULT_ROWS) + } + onDeselect={() => setAttributes({ rows: DEFAULT_ROWS })} + isShownByDefault + > + {rows.map((row, rowIndex) => ( + + updateRow(rowIndex, { tooltip: value }) + } + placeholder={__('Tooltip text…', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + ))} + +
    +
    + + {/* Block Content */} +
    +
    + + {/* Header Row */} + + + {/* Feature label column header */} + + + {/* Column headers */} + {columns.map((col, colIndex) => ( + + ))} + + + + {/* Data Rows */} + + {rows.map((row, rowIndex) => ( + + {/* Feature label */} + + + {/* Cells */} + {row.cells.map((cell, colIndex) => ( + + ))} + + ))} + +
    + + {__('Features', 'airo-wp')} + + + {col.featured && ( + + {__('Popular', 'airo-wp')} + + )} + + updateColumn(colIndex, { + name: value, + }) + } + placeholder={__( + 'Plan Name', + 'airo-wp' + )} + allowedFormats={[]} + /> + + {showCtaButtons && ( + + updateColumn(colIndex, { + linkText: value, + }) + } + placeholder={__( + 'CTA Text', + 'airo-wp' + )} + allowedFormats={[]} + /> + )} +
    +
    + + updateRow(rowIndex, { + label: value, + }) + } + placeholder={__( + 'Feature name', + 'airo-wp' + )} + allowedFormats={[ + 'core/bold', + 'core/italic', + ]} + /> + {row.tooltip && ( + + + ? + + + )} +
    + + {/* Row controls */} +
    + + + {rows.length > 1 && ( + + )} +
    +
    + setSelectedCell({ + row: rowIndex, + col: colIndex, + }) + } + onKeyDown={(e) => { + if ( + e.key === 'Enter' || + e.key === ' ' + ) { + setSelectedCell({ + row: rowIndex, + col: colIndex, + }); + } + }} + role="button" + tabIndex="0" + aria-label={`${row.label || __('Feature', 'airo-wp')}, ${columns[colIndex]?.name || __('Column', 'airo-wp')}`} + > +
    + +
    + + {/* Cell type toggle */} + {selectedCell?.row === rowIndex && + selectedCell?.col === + colIndex && ( +
    + + + +
    + )} +
    + + {/* Add Row Button */} +
    + +
    +
    +
    + + ); +} diff --git a/src/blocks/comparison-table/editor.scss b/src/blocks/comparison-table/editor.scss new file mode 100644 index 0000000..762d455 --- /dev/null +++ b/src/blocks/comparison-table/editor.scss @@ -0,0 +1,202 @@ +/** + * Comparison Table - Editor-only Styles + * + * Styles that apply only in the block editor for editing UI, + * row controls, cell toolbars, and selection states. + */ + +// Ensure the block is selectable in the editor +.wp-block-airo-wp-comparison-table { + min-height: 100px; +} + +// Row controls and editor UI +.airo-wp-comparison-table-editor { + // Modern floating row controls (move up/down, delete) + &__row-controls { + display: none; + align-items: center; + gap: 2px; + margin-top: 6px; + padding: 3px; + background: #fff; + border-radius: 20px; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + 0 0 0 1px rgba(0, 0, 0, 0.04); + width: fit-content; + } + + &__row-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: none; + border-radius: 50%; + background: transparent; + color: #616161; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; + outline: none; + line-height: 1; + + svg { + display: block; + } + + &:hover { + background: #f0f0f0; + color: #1e1e1e; + } + + &:active { + background: #e0e0e0; + } + + &:focus-visible { + box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #007cba); + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + + &:hover { + background: transparent; + color: #616161; + } + } + + &--delete { + color: #b32d2e; + margin-left: 2px; + + &::before { + content: ""; + position: absolute; + left: -3px; + top: 4px; + bottom: 4px; + width: 1px; + background: #e0e0e0; + } + + position: relative; + + &:hover { + background: #fcecec; + color: #8c1a1b; + } + + &:active { + background: #f5d5d5; + } + } + } + + // Add row button + &__add-row { + display: flex; + justify-content: center; + padding: 12px 0; + opacity: 0.6; + transition: opacity 0.2s ease; + + &:hover { + opacity: 1; + } + } + + // Column settings in inspector + &__column-settings { + + h4 { + font-size: 13px; + } + } + + // Cell type toolbar popup + &__cell-toolbar { + position: absolute; + bottom: calc(100% + 4px); + left: 50%; + transform: translateX(-50%); + z-index: 20; + display: flex; + align-items: center; + gap: 2px; + padding: 3px; + background: #fff; + border-radius: 20px; + box-shadow: + 0 2px 12px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(0, 0, 0, 0.04); + white-space: nowrap; + } + + &__type-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 28px; + padding: 0 8px; + border: none; + border-radius: 14px; + background: transparent; + color: #616161; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; + outline: none; + + &:hover { + background: #f0f0f0; + color: #1e1e1e; + } + + &.is-active { + background: var(--wp-admin-theme-color, #007cba); + color: #fff; + } + + &:focus-visible { + box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #007cba); + } + } +} + +// Show row controls on row hover in editor +.airo-wp-comparison-table__row:hover { + + .airo-wp-comparison-table-editor__row-controls { + display: flex; + } +} + +// Cell hover state in editor +.airo-wp-comparison-table__cell:not(.airo-wp-comparison-table__cell--label) { + cursor: pointer; + position: relative; + + &:hover { + outline: 2px solid var(--wp-admin-theme-color, #007cba); + outline-offset: -2px; + border-radius: 2px; + } +} + +// Make RichText inputs in the table less distracting +// stylelint-disable-next-line selector-class-pattern +.airo-wp-comparison-table .rich-text:focus { + outline: none; +} + +// Tooltip trigger in editor +.airo-wp-comparison-table__tooltip-trigger { + user-select: none; +} diff --git a/src/blocks/comparison-table/index.js b/src/blocks/comparison-table/index.js new file mode 100644 index 0000000..2aa398a --- /dev/null +++ b/src/blocks/comparison-table/index.js @@ -0,0 +1,42 @@ +/** + * Comparison Table Block + * + * Feature comparison table with dynamic columns, multiple cell types, + * and responsive design for comparing products, services, or plans. + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; +import { ICON_COLOR } from '../shared/constants'; +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, +}); diff --git a/src/blocks/comparison-table/save.js b/src/blocks/comparison-table/save.js new file mode 100644 index 0000000..38254cf --- /dev/null +++ b/src/blocks/comparison-table/save.js @@ -0,0 +1,233 @@ +/** + * Comparison Table Block - Save Component + * + * Generates the static frontend HTML for the comparison table. + * Includes data attributes for tooltip behavior handled by view.js. + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, RichText } from '@wordpress/block-editor'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +/** + * Save component for the Comparison Table block + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @return {JSX.Element} Comparison Table save markup + */ +export default function ComparisonTableSave({ attributes }) { + const { + columns, + rows, + alternatingRows, + responsiveMode, + featuredColumnColor, + headerBackgroundColor, + headerTextColor, + showCtaButtons, + ctaStyle, + } = attributes; + + const blockProps = useBlockProps.save({ + className: [ + 'airo-wp-comparison-table', + alternatingRows && 'airo-wp-comparison-table--alternating', + responsiveMode === 'stack' && + 'airo-wp-comparison-table--responsive-stack', + responsiveMode === 'scroll' && + 'airo-wp-comparison-table--responsive-scroll', + ] + .filter(Boolean) + .join(' '), + style: { + ...(featuredColumnColor && { + '--airo-wp-comparison-featured-color': + convertColorToCSSVar(featuredColumnColor), + }), + ...(headerBackgroundColor && { + '--airo-wp-comparison-header-bg': convertColorToCSSVar( + headerBackgroundColor + ), + }), + ...(headerTextColor && { + '--airo-wp-comparison-header-text': + convertColorToCSSVar(headerTextColor), + }), + }, + }); + + return ( +
    +
    + + {/* Header Row */} + + + {/* Feature label column header */} + + + {/* Column headers */} + {columns.map((col, colIndex) => ( + + ))} + + + + {/* Data Rows */} + + {rows.map((row, rowIndex) => ( + + {/* Feature label */} + + + {/* Cells */} + {row.cells.map((cell, colIndex) => ( + + ))} + + ))} + +
    + {col.featured && ( + + {__('Popular', 'airo-wp')} + + )} + + + + {showCtaButtons && col.link && ( + + {col.linkText || + __( + 'Get Started', + 'airo-wp' + )} + + )} + + {showCtaButtons && + !col.link && + col.linkText && ( + + {col.linkText} + + )} +
    +
    + + {row.tooltip && ( + + ? + + )} +
    +
    +
    + {cell.type === 'check' && ( + + + + )} + + {cell.type === 'cross' && ( + + + + + )} + + {cell.type === 'text' && ( + + )} +
    +
    +
    +
    + ); +} diff --git a/src/blocks/comparison-table/style.scss b/src/blocks/comparison-table/style.scss new file mode 100644 index 0000000..68b98ed --- /dev/null +++ b/src/blocks/comparison-table/style.scss @@ -0,0 +1,419 @@ +/** + * Comparison Table - Frontend & Editor Styles + * + * Applied in both the block editor and the frontend. + * Uses BEM naming convention with .airo-wp-comparison-table prefix. + */ + +.airo-wp-comparison-table { + box-sizing: border-box; + max-width: 100%; + color: var(--wp--preset--color--contrast, inherit); + + *, + *::before, + *::after { + box-sizing: border-box; + } + + // Wrapper handles overflow for responsive scroll mode + &__wrapper { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + // Table base styles + &__table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; + font-size: inherit; + line-height: inherit; + } + + // --- Header --- + + &__header { + + tr { + border-bottom: 2px solid currentcolor; + opacity: 0.9; + } + } + + &__header-cell { + padding: 1.25rem 1rem; + text-align: center; + vertical-align: bottom; + font-weight: 600; + background-color: var(--airo-wp-comparison-header-bg, transparent); + color: var(--airo-wp-comparison-header-text, currentcolor); + position: relative; + + &--label { + text-align: left; + width: 30%; + min-width: 140px; + } + + &--featured { + background-color: var( + --airo-wp-comparison-featured-color, + var(--wp--preset--color--primary, #2563eb) + ); + color: #fff; + } + } + + &__column-name { + display: block; + font-size: 1.25em; + font-weight: 700; + margin-bottom: 0.5rem; + } + + &__featured-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0.2em 0.6em; + border-radius: 100px; + background-color: rgba(255, 255, 255, 0.2); + margin-bottom: 0.5rem; + } + + // --- CTA Buttons --- + + &__cta { + display: inline-block; + margin-top: 0.75rem; + padding: 0.5em 1.25em; + font-size: 0.85em; + font-weight: 600; + border-radius: 6px; + text-decoration: none; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease, + box-shadow 0.2s ease; + line-height: 1.4; + + &--filled { + background-color: currentcolor; + color: var(--airo-wp-comparison-header-bg, #fff); + + // Use a pseudo-element for text color to avoid inheriting currentcolor + &, + &:visited { + text-decoration: none; + } + } + + &--outlined { + background-color: transparent; + border: 2px solid currentcolor; + color: currentcolor; + + &, + &:visited { + text-decoration: none; + } + } + } + + // Featured column CTA adjustments + &__header-cell--featured &__cta { + + &--filled { + background-color: #fff; + color: var( + --airo-wp-comparison-featured-color, + var(--wp--preset--color--primary, #2563eb) + ); + } + + &--outlined { + border-color: #fff; + color: #fff; + } + } + + // --- Body --- + + &__body { + + tr { + border-bottom: 1px solid rgba(128, 128, 128, 0.2); + transition: background-color 0.15s ease; + + &:last-child { + border-bottom: none; + } + + &:hover { + background-color: rgba(128, 128, 128, 0.05); + } + } + } + + // Alternating row colors + &--alternating &__body tr:nth-child(even) { + background-color: rgba(128, 128, 128, 0.04); + + &:hover { + background-color: rgba(128, 128, 128, 0.08); + } + } + + // --- Cells --- + + &__cell { + padding: 0.875rem 1rem; + text-align: center; + vertical-align: middle; + + &--label { + text-align: left; + font-weight: 500; + } + + &--featured { + background-color: rgba( + var(--airo-wp-comparison-featured-rgb, 37, 99, 235), + 0.04 + ); + // Use a subtle overlay instead for simpler implementation + position: relative; + + &::before { + content: ""; + position: absolute; + inset: 0; + background-color: var( + --airo-wp-comparison-featured-color, + var(--wp--preset--color--primary, #2563eb) + ); + opacity: 0.04; + pointer-events: none; + } + } + } + + &__cell-content { + display: flex; + align-items: center; + justify-content: center; + } + + &__cell--label &__cell-content { + justify-content: flex-start; + } + + &__cell-text { + line-height: 1.5; + } + + // --- Icons --- + + &__icon { + flex-shrink: 0; + + &--check { + color: var( + --wp--preset--color--vivid-green-cyan, + #00d084 + ); + } + + &--cross { + color: rgba(128, 128, 128, 0.6); + } + } + + // --- Labels & Tooltips --- + + &__label-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + } + + &__row-label { + line-height: 1.5; + } + + &__tooltip-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 11px; + font-weight: 700; + line-height: 1; + border-radius: 50%; + background-color: rgba(128, 128, 128, 0.15); + color: inherit; + opacity: 0.6; + cursor: help; + flex-shrink: 0; + position: relative; + transition: opacity 0.2s ease; + + &:hover { + opacity: 1; + } + } + + // Frontend tooltip popup (positioned by view.js) + &__tooltip { + position: absolute; + z-index: 100; + max-width: 240px; + padding: 0.5em 0.75em; + font-size: 0.8em; + font-weight: 400; + line-height: 1.4; + color: #fff; + background-color: rgba(0, 0, 0, 0.85); + border-radius: 6px; + pointer-events: none; + opacity: 0; + transform: translateY(4px); + transition: opacity 0.15s ease, transform 0.15s ease; + white-space: normal; + + &.is-visible { + opacity: 1; + transform: translateY(0); + } + } + + // --- Responsive: Horizontal Scroll --- + + &--responsive-scroll { + + .airo-wp-comparison-table__wrapper { + overflow-x: auto; + } + + .airo-wp-comparison-table__table { + min-width: 600px; + } + } + + // --- Responsive: Stack on Mobile --- + + &--responsive-stack { + + @media (max-width: 767px) { + + .airo-wp-comparison-table__table, + .airo-wp-comparison-table__header, + .airo-wp-comparison-table__body, + .airo-wp-comparison-table__header tr, + .airo-wp-comparison-table__body tr, + .airo-wp-comparison-table__header-cell, + .airo-wp-comparison-table__cell { + display: block; + width: 100%; + } + + .airo-wp-comparison-table__header { + + tr { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding-bottom: 1rem; + margin-bottom: 0.5rem; + border-bottom: 2px solid currentcolor; + opacity: 0.9; + } + } + + .airo-wp-comparison-table__header-cell { + flex: 1; + min-width: 0; + padding: 0.75rem; + + &--label { + flex: 0 0 100%; + text-align: center; + font-weight: 700; + padding-bottom: 0; + } + } + + .airo-wp-comparison-table__row { + display: grid; + grid-template-columns: 1fr; + gap: 0; + padding: 0.75rem 0; + border-bottom: 1px solid rgba(128, 128, 128, 0.2); + } + + .airo-wp-comparison-table__cell { + display: flex; + align-items: center; + justify-content: space-between; + text-align: right; + padding: 0.5rem 0.75rem; + + &--label { + text-align: left; + font-weight: 700; + font-size: 1.05em; + padding-bottom: 0.25rem; + border-bottom: none; + } + + // Show column label on mobile + &:not(.airo-wp-comparison-table__cell--label)::before { + content: attr(data-label); + font-weight: 500; + text-align: left; + margin-right: 1rem; + opacity: 0.7; + flex-shrink: 0; + } + } + + .airo-wp-comparison-table__cell-content { + justify-content: flex-end; + } + + // Featured column highlight in stacked mode + .airo-wp-comparison-table__cell--featured { + + &::before { + display: none; + } + + background-color: rgba(128, 128, 128, 0.04); + border-radius: 4px; + } + } + } + + // --- General Mobile Adjustments --- + + @media (max-width: 767px) { + + &__column-name { + font-size: 1.1em; + } + + &__cta { + font-size: 0.8em; + padding: 0.4em 1em; + } + + &__featured-badge { + font-size: 0.65em; + } + } +} diff --git a/src/blocks/comparison-table/view.js b/src/blocks/comparison-table/view.js new file mode 100644 index 0000000..ea41cf4 --- /dev/null +++ b/src/blocks/comparison-table/view.js @@ -0,0 +1,91 @@ +/** + * Comparison Table Block - Frontend Interactivity + * + * Handles tooltip display on hover/focus with accessibility support. + */ + +let tooltipCounter = 0; + +function initComparisonTables() { + const tables = document.querySelectorAll('.airo-wp-comparison-table'); + + tables.forEach((table) => { + if (table.dataset.dsgoInitialized) { + return; + } + table.dataset.dsgoInitialized = 'true'; + + initTooltips(table); + }); +} + +document.addEventListener('DOMContentLoaded', initComparisonTables); +document.addEventListener('airo-wp-content-loaded', initComparisonTables); + +/** + * Initializes tooltip behavior for feature row info icons + * + * @param {HTMLElement} table - The comparison table wrapper element + */ +function initTooltips(table) { + const triggers = table.querySelectorAll( + '.airo-wp-comparison-table__tooltip-trigger[data-tooltip]' + ); + + if (!triggers.length) { + return; + } + + triggers.forEach((trigger) => { + const tooltipText = trigger.dataset.tooltip; + if (!tooltipText) { + return; + } + + // Set ARIA attributes with counter-based unique ID + const tooltipId = `airo-wp-tooltip-${++tooltipCounter}`; + trigger.setAttribute('aria-describedby', tooltipId); + trigger.setAttribute('tabindex', '0'); + trigger.setAttribute('role', 'button'); + + // Create tooltip element + // textContent is used (not innerHTML) to prevent XSS from user-supplied tooltip text + const tooltip = document.createElement('div'); + tooltip.className = 'airo-wp-comparison-table__tooltip'; + tooltip.id = tooltipId; + tooltip.setAttribute('role', 'tooltip'); + tooltip.textContent = tooltipText; + trigger.parentNode.style.position = 'relative'; + trigger.parentNode.appendChild(tooltip); + + /** + * Shows the tooltip + */ + const show = () => { + tooltip.classList.add('is-visible'); + }; + + /** + * Hides the tooltip + */ + const hide = () => { + tooltip.classList.remove('is-visible'); + }; + + // Mouse events + trigger.addEventListener('mouseenter', show); + trigger.addEventListener('mouseleave', hide); + + // Keyboard events + trigger.addEventListener('focus', show); + trigger.addEventListener('blur', hide); + + // Dismiss on Escape + trigger.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + hide(); + trigger.blur(); + } + }); + }); +} diff --git a/src/blocks/countdown-timer/block.json b/src/blocks/countdown-timer/block.json new file mode 100644 index 0000000..8712423 --- /dev/null +++ b/src/blocks/countdown-timer/block.json @@ -0,0 +1,155 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/countdown-timer", + "version": "1.0.0", + "title": "Countdown Timer", + "category": "airo-wp", + "description": "Display a countdown timer to a specific date and time with customizable styling.", + "keywords": [ + "countdown", + "timer", + "clock", + "deadline", + "event" + ], + "textdomain": "airo-wp", + "icon": "clock", + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true + } + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": false, + "text": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "textAlign": true, + "__experimentalDefaultControls": { + "fontSize": true, + "textAlign": true + }, + "__experimentalFontWeight": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true, + "width": true + } + } + }, + "attributes": { + "targetDateTime": { + "type": "string", + "default": "" + }, + "timezone": { + "type": "string", + "default": "" + }, + "showDays": { + "type": "boolean", + "default": true + }, + "showHours": { + "type": "boolean", + "default": true + }, + "showMinutes": { + "type": "boolean", + "default": true + }, + "showSeconds": { + "type": "boolean", + "default": true + }, + "layout": { + "type": "string", + "default": "boxed", + "enum": [ + "boxed", + "inline", + "compact" + ] + }, + "completionAction": { + "type": "string", + "default": "message", + "enum": [ + "message", + "hide" + ] + }, + "completionMessage": { + "type": "string", + "default": "The countdown has ended!" + }, + "numberColor": { + "type": "string", + "default": "" + }, + "labelColor": { + "type": "string", + "default": "" + }, + "unitBackgroundColor": { + "type": "string", + "default": "" + }, + "unitBorder": { + "type": "object", + "default": { + "color": "", + "style": "solid", + "width": "2px" + } + }, + "unitBorderRadius": { + "type": "number", + "default": 12 + }, + "unitGap": { + "type": "string", + "default": "1rem" + }, + "unitPadding": { + "type": "string", + "default": "1.5rem" + } + }, + "example": { + "attributes": { + "targetDateTime": "2025-12-31T23:59:59", + "showDays": true, + "showHours": true, + "showMinutes": true, + "showSeconds": true, + "layout": "boxed" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "viewScript": "file:./view.js" +} diff --git a/src/blocks/countdown-timer/components/inspector/CompletionPanel.js b/src/blocks/countdown-timer/components/inspector/CompletionPanel.js new file mode 100644 index 0000000..959ffc5 --- /dev/null +++ b/src/blocks/countdown-timer/components/inspector/CompletionPanel.js @@ -0,0 +1,88 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { RadioControl, TextControl } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +const DEFAULT_COMPLETION_MESSAGE = 'The countdown has ended!'; + +/** + * Completion Panel component + * + * Renders DsgoInspectorPanel.Item entries for the end-of-countdown + * behaviour. Meant to be composed inside the Settings DsgoInspectorPanel + * in countdown-timer/edit.js. + * + * @param {Object} props - Component properties + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @return {JSX.Element} Item fragment + */ +export default function CompletionPanel({ attributes, setAttributes }) { + const { completionAction, completionMessage } = attributes; + + return ( + <> + completionAction !== 'message'} + onDeselect={() => + setAttributes({ completionAction: 'message' }) + } + isShownByDefault + > + + setAttributes({ completionAction: value }) + } + /> + + + {completionAction === 'message' && ( + + completionMessage !== DEFAULT_COMPLETION_MESSAGE + } + onDeselect={() => + setAttributes({ + completionMessage: DEFAULT_COMPLETION_MESSAGE, + }) + } + isShownByDefault + > + + setAttributes({ completionMessage: value }) + } + placeholder={__( + 'The countdown has ended!', + 'airo-wp' + )} + help={__( + 'This message will replace the countdown timer when it reaches zero.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + ); +} diff --git a/src/blocks/countdown-timer/components/inspector/DateTimePanel.js b/src/blocks/countdown-timer/components/inspector/DateTimePanel.js new file mode 100644 index 0000000..167a68c --- /dev/null +++ b/src/blocks/countdown-timer/components/inspector/DateTimePanel.js @@ -0,0 +1,149 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { DateTimePicker, SelectControl, Notice } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +/** + * Common timezone options - WordPress standard timezones + */ +const TIMEZONE_OPTIONS = [ + { label: __('WordPress Default', 'airo-wp'), value: '' }, + { label: 'UTC', value: 'UTC' }, + // North America + { label: 'America/New_York (EST/EDT)', value: 'America/New_York' }, + { label: 'America/Chicago (CST/CDT)', value: 'America/Chicago' }, + { label: 'America/Denver (MST/MDT)', value: 'America/Denver' }, + { label: 'America/Phoenix (MST)', value: 'America/Phoenix' }, + { label: 'America/Los_Angeles (PST/PDT)', value: 'America/Los_Angeles' }, + { label: 'America/Anchorage (AKST/AKDT)', value: 'America/Anchorage' }, + { label: 'America/Adak (HST/HDT)', value: 'America/Adak' }, + { label: 'Pacific/Honolulu (HST)', value: 'Pacific/Honolulu' }, + { label: 'America/Toronto', value: 'America/Toronto' }, + { label: 'America/Vancouver', value: 'America/Vancouver' }, + { label: 'America/Mexico_City', value: 'America/Mexico_City' }, + // South America + { label: 'America/Sao_Paulo', value: 'America/Sao_Paulo' }, + { label: 'America/Buenos_Aires', value: 'America/Buenos_Aires' }, + { label: 'America/Santiago', value: 'America/Santiago' }, + { label: 'America/Bogota', value: 'America/Bogota' }, + { label: 'America/Lima', value: 'America/Lima' }, + // Europe + { label: 'Europe/London (GMT/BST)', value: 'Europe/London' }, + { label: 'Europe/Paris (CET/CEST)', value: 'Europe/Paris' }, + { label: 'Europe/Berlin', value: 'Europe/Berlin' }, + { label: 'Europe/Rome', value: 'Europe/Rome' }, + { label: 'Europe/Madrid', value: 'Europe/Madrid' }, + { label: 'Europe/Amsterdam', value: 'Europe/Amsterdam' }, + { label: 'Europe/Brussels', value: 'Europe/Brussels' }, + { label: 'Europe/Vienna', value: 'Europe/Vienna' }, + { label: 'Europe/Warsaw', value: 'Europe/Warsaw' }, + { label: 'Europe/Athens', value: 'Europe/Athens' }, + { label: 'Europe/Istanbul', value: 'Europe/Istanbul' }, + { label: 'Europe/Moscow', value: 'Europe/Moscow' }, + // Asia + { label: 'Asia/Dubai', value: 'Asia/Dubai' }, + { label: 'Asia/Karachi', value: 'Asia/Karachi' }, + { label: 'Asia/Kolkata (IST)', value: 'Asia/Kolkata' }, + { label: 'Asia/Bangkok', value: 'Asia/Bangkok' }, + { label: 'Asia/Singapore', value: 'Asia/Singapore' }, + { label: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' }, + { label: 'Asia/Shanghai', value: 'Asia/Shanghai' }, + { label: 'Asia/Tokyo (JST)', value: 'Asia/Tokyo' }, + { label: 'Asia/Seoul', value: 'Asia/Seoul' }, + { label: 'Asia/Jakarta', value: 'Asia/Jakarta' }, + { label: 'Asia/Manila', value: 'Asia/Manila' }, + // Middle East + { label: 'Asia/Jerusalem', value: 'Asia/Jerusalem' }, + { label: 'Asia/Riyadh', value: 'Asia/Riyadh' }, + { label: 'Asia/Qatar', value: 'Asia/Qatar' }, + // Africa + { label: 'Africa/Cairo', value: 'Africa/Cairo' }, + { label: 'Africa/Johannesburg', value: 'Africa/Johannesburg' }, + { label: 'Africa/Lagos', value: 'Africa/Lagos' }, + { label: 'Africa/Nairobi', value: 'Africa/Nairobi' }, + // Australia & Pacific + { label: 'Australia/Sydney (AEDT/AEST)', value: 'Australia/Sydney' }, + { label: 'Australia/Melbourne', value: 'Australia/Melbourne' }, + { label: 'Australia/Brisbane', value: 'Australia/Brisbane' }, + { label: 'Australia/Perth', value: 'Australia/Perth' }, + { label: 'Pacific/Auckland (NZDT/NZST)', value: 'Pacific/Auckland' }, + { label: 'Pacific/Fiji', value: 'Pacific/Fiji' }, +]; + +/** + * DateTime Panel component + * + * Renders DsgoInspectorPanel.Item entries for target-date + timezone. + * Meant to be composed inside the Settings DsgoInspectorPanel in + * countdown-timer/edit.js. + * + * @param {Object} props - Component properties + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @return {JSX.Element} Item fragment + */ +export default function DateTimePanel({ attributes, setAttributes }) { + const { targetDateTime, timezone } = attributes; + + // Get WordPress timezone setting + const wpTimezone = + window?.wp?.date?.getSettings?.()?.timezone?.string || 'UTC'; + + return ( + <> + !!targetDateTime} + onDeselect={() => setAttributes({ targetDateTime: '' })} + isShownByDefault + > + + {__( + 'Set the target date and time for your countdown.', + 'airo-wp' + )} + +
    + + setAttributes({ targetDateTime: newDateTime }) + } + is12Hour={true} + /> +
    + {targetDateTime && ( + + {__('Target date set!', 'airo-wp')}{' '} + {new Date(targetDateTime).toLocaleString()} + + )} +
    + + timezone !== ''} + onDeselect={() => setAttributes({ timezone: '' })} + isShownByDefault + > + + setAttributes({ timezone: newTimezone }) + } + help={ + __('WordPress site timezone:', 'airo-wp') + + ' ' + + wpTimezone + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + ); +} diff --git a/src/blocks/countdown-timer/components/inspector/DisplayPanel.js b/src/blocks/countdown-timer/components/inspector/DisplayPanel.js new file mode 100644 index 0000000..c015ef3 --- /dev/null +++ b/src/blocks/countdown-timer/components/inspector/DisplayPanel.js @@ -0,0 +1,134 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToggleControl, SelectControl, Notice } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +/** + * Layout options + */ +const LAYOUT_OPTIONS = [ + { + label: __('Boxed (Default)', 'airo-wp'), + value: 'boxed', + }, + { + label: __('Inline', 'airo-wp'), + value: 'inline', + }, + { + label: __('Compact', 'airo-wp'), + value: 'compact', + }, +]; + +/** + * Display Panel component + * + * Renders DsgoInspectorPanel.Item entries for layout + unit visibility. + * Meant to be composed inside the Settings DsgoInspectorPanel in + * countdown-timer/edit.js. + * + * @param {Object} props - Component properties + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @return {JSX.Element} Item fragment + */ +export default function DisplayPanel({ attributes, setAttributes }) { + const { showDays, showHours, showMinutes, showSeconds, layout } = + attributes; + + // Check if at least one unit is visible + const hasVisibleUnit = showDays || showHours || showMinutes || showSeconds; + + return ( + <> + layout !== 'boxed'} + onDeselect={() => setAttributes({ layout: 'boxed' })} + isShownByDefault + > + + setAttributes({ layout: newLayout }) + } + help={__( + 'Choose how the countdown units are displayed.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + showDays !== true} + onDeselect={() => setAttributes({ showDays: true })} + isShownByDefault + > + setAttributes({ showDays: value })} + __nextHasNoMarginBottom + /> + + + showHours !== true} + onDeselect={() => setAttributes({ showHours: true })} + isShownByDefault + > + setAttributes({ showHours: value })} + __nextHasNoMarginBottom + /> + + + showMinutes !== true} + onDeselect={() => setAttributes({ showMinutes: true })} + isShownByDefault + > + setAttributes({ showMinutes: value })} + __nextHasNoMarginBottom + /> + + + showSeconds !== true} + onDeselect={() => setAttributes({ showSeconds: true })} + isShownByDefault + > + setAttributes({ showSeconds: value })} + __nextHasNoMarginBottom + /> + {!hasVisibleUnit && ( + + {__( + 'At least one time unit should be visible.', + 'airo-wp' + )} + + )} + + + ); +} diff --git a/src/blocks/countdown-timer/components/inspector/StylingPanel.js b/src/blocks/countdown-timer/components/inspector/StylingPanel.js new file mode 100644 index 0000000..2db311b --- /dev/null +++ b/src/blocks/countdown-timer/components/inspector/StylingPanel.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +/** + * Styling Panel component + * + * Renders DsgoInspectorPanel.Item entries for countdown unit spacing. + * Meant to be composed inside the Settings DsgoInspectorPanel in + * countdown-timer/edit.js. + * + * @param {Object} props - Component properties + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @return {JSX.Element} Item fragment + */ +export default function StylingPanel({ attributes, setAttributes }) { + const { unitGap, unitPadding } = attributes; + + return ( + <> + unitGap !== '1rem'} + onDeselect={() => setAttributes({ unitGap: '1rem' })} + isShownByDefault + > + setAttributes({ unitGap: value })} + units={[ + { value: 'px', label: 'px' }, + { value: 'rem', label: 'rem' }, + { value: 'em', label: 'em' }, + ]} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + unitPadding !== '1.5rem'} + onDeselect={() => setAttributes({ unitPadding: '1.5rem' })} + isShownByDefault + > + setAttributes({ unitPadding: value })} + units={[ + { value: 'px', label: 'px' }, + { value: 'rem', label: 'rem' }, + { value: 'em', label: 'em' }, + ]} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + ); +} diff --git a/src/blocks/countdown-timer/components/inspector/UnitBorderPanel.js b/src/blocks/countdown-timer/components/inspector/UnitBorderPanel.js new file mode 100644 index 0000000..ce515d4 --- /dev/null +++ b/src/blocks/countdown-timer/components/inspector/UnitBorderPanel.js @@ -0,0 +1,87 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalBorderControl as BorderControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +// Matches `attributes.unitBorder.default` in block.json. Exported so +// countdown-timer/edit.js can reference the same object in its +// panel-level resetAll — no drift between the per-item and panel-wide +// resets. +export const DEFAULT_UNIT_BORDER = { + color: '', + style: 'solid', + width: '2px', +}; + +/** + * Unit Border Panel component + * + * Renders DsgoInspectorPanel.Item entries for per-unit border + radius. + * Originally lived in as a nested + * ToolsPanel; now composed into the Settings panel in + * countdown-timer/edit.js to match the rest of the plugin convention. + * + * @param {Object} props - Component properties + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @return {JSX.Element} Item fragment + */ +export default function UnitBorderPanel({ attributes, setAttributes }) { + const { unitBorder, unitBorderRadius } = attributes; + + return ( + <> + + !!unitBorder && + (unitBorder.color !== '' || + unitBorder.style !== 'solid' || + unitBorder.width !== '2px') + } + onDeselect={() => + setAttributes({ unitBorder: DEFAULT_UNIT_BORDER }) + } + isShownByDefault + > + setAttributes({ unitBorder: value })} + withSlider={true} + __next40pxDefaultSize + /> + + + unitBorderRadius !== 12} + onDeselect={() => setAttributes({ unitBorderRadius: 12 })} + isShownByDefault + > + { + const numValue = parseInt(value); + setAttributes({ + unitBorderRadius: isNaN(numValue) ? 12 : numValue, + }); + }} + units={[{ value: 'px', label: 'px' }]} + min={0} + max={50} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + ); +} diff --git a/src/blocks/countdown-timer/deprecated.js b/src/blocks/countdown-timer/deprecated.js new file mode 100644 index 0000000..1abb0f0 --- /dev/null +++ b/src/blocks/countdown-timer/deprecated.js @@ -0,0 +1,807 @@ +/** + * WordPress dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import { + convertPresetToCSSVar, + convertColorToCSSVar, +} from '../../utils/convert-preset-to-css-var'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Internal dependencies + */ +// formatTimeUnit not used in deprecated - kept for potential future deprecations +// import { formatTimeUnit } from './utils/time-calculator'; + +/** + * Shared supports for all deprecated versions. + * Uses __experimentalBorder (the historical name) instead of border. + */ +const sharedSupports = { + anchor: true, + align: ['wide', 'full'], + spacing: { + margin: true, + padding: true, + blockGap: true, + __experimentalDefaultControls: { + padding: true, + }, + }, + color: { + background: true, + text: true, + gradients: true, + __experimentalDefaultControls: { + background: false, + text: false, + }, + }, + typography: { + fontSize: true, + lineHeight: true, + fontWeight: true, + textAlign: true, + __experimentalDefaultControls: { + fontSize: true, + textAlign: true, + }, + }, + __experimentalBorder: { + color: true, + radius: true, + style: true, + width: true, + __experimentalDefaultControls: { + radius: true, + width: true, + }, + }, +}; + +/** + * Deprecated version 2: Pre-BorderControl migration + * + * This deprecation handles blocks created before we migrated to WordPress BorderControl. + * + * Changes in current version: + * - Replaced unitBorderColor and unitBorderWidth with unitBorder object + * - Unit border now uses WordPress core BorderControl component + */ +/** + * Attributes/supports as of v3 — identical to the current block. Only the + * MARKUP changed, so these are re-declared (not migrated) below. + * + * NOTE: this deliberately does NOT reuse `sharedSupports` above, which declares + * typography.fontWeight with the un-prefixed key. WordPress's hasBlockSupport() + * only recognises `__experimentalFontWeight`, so the un-prefixed name silently + * fails — the support looks declared but isn't. block.json uses the prefixed + * key, so this version must too. + */ +const v3Supports = { + anchor: true, + align: ['wide', 'full'], + spacing: { + margin: true, + padding: true, + blockGap: true, + __experimentalDefaultControls: { padding: true }, + }, + color: { + background: true, + text: true, + gradients: true, + __experimentalDefaultControls: { background: false, text: false }, + }, + typography: { + fontSize: true, + lineHeight: true, + textAlign: true, + __experimentalDefaultControls: { fontSize: true, textAlign: true }, + __experimentalFontWeight: true, + }, + __experimentalBorder: { + color: true, + radius: true, + style: true, + width: true, + __experimentalDefaultControls: { radius: true, width: true }, + }, +}; + +/** + * Inline `display:none` on the completion message — the version before that + * constant moved to style.scss. + * + * The message div serialized `style="display:none"` into every saved timer. + * style.scss already declared `display: none` on + * `.airo-wp-countdown-timer__completion-message`, and view.js reveals the message + * by setting `style.display = 'block'` (an inline style, which beats the + * stylesheet either way) — so the baked copy was pure duplication. + * + * Markup-only change: migrate() is a passthrough. + * + * NOTE: WordPress calls isEligible(attributes, innerBlocks, { blockNode, block }) + * — there is no `innerHTML` key on that third argument. + */ +const v3 = { + supports: v3Supports, + attributes: { + targetDateTime: { type: 'string', default: '' }, + timezone: { type: 'string', default: '' }, + showDays: { type: 'boolean', default: true }, + showHours: { type: 'boolean', default: true }, + showMinutes: { type: 'boolean', default: true }, + showSeconds: { type: 'boolean', default: true }, + layout: { + type: 'string', + default: 'boxed', + enum: ['boxed', 'inline', 'compact'], + }, + completionAction: { + type: 'string', + default: 'message', + enum: ['message', 'hide'], + }, + completionMessage: { + type: 'string', + default: 'The countdown has ended!', + }, + numberColor: { type: 'string', default: '' }, + labelColor: { type: 'string', default: '' }, + unitBackgroundColor: { type: 'string', default: '' }, + unitBorder: { + type: 'object', + default: { color: '', style: 'solid', width: '2px' }, + }, + unitBorderRadius: { type: 'number', default: 12 }, + unitGap: { type: 'string', default: '1rem' }, + unitPadding: { type: 'string', default: '1.5rem' }, + }, + isEligible(attributes, innerBlocks, extra) { + const html = getDeprecatedBlockHTML(extra); + return ( + html.includes('airo-wp-countdown-timer__completion-message') && + html.includes('display:none') + ); + }, + migrate(attributes) { + // Markup-only change. + return attributes; + }, + save({ attributes }) { + const { + targetDateTime, + timezone, + showDays, + showHours, + showMinutes, + showSeconds, + layout, + completionAction, + completionMessage, + numberColor, + labelColor, + unitBackgroundColor, + unitBorder, + unitBorderRadius, + unitGap, + unitPadding, + } = attributes; + + const unitStyle = { + backgroundColor: + convertColorToCSSVar(unitBackgroundColor) || 'transparent', + borderColor: + unitBorder?.color || + 'var(--wp--preset--color--accent-2, currentColor)', + borderWidth: unitBorder?.width || '2px', + borderStyle: unitBorder?.style || 'solid', + borderRadius: `${unitBorderRadius}px`, + padding: unitPadding || '1.5rem', + }; + + const numberStyle = { + color: + convertColorToCSSVar(numberColor) || + 'var(--wp--preset--color--accent-2, currentColor)', + }; + + const labelStyle = { + color: convertColorToCSSVar(labelColor) || 'currentColor', + }; + + const containerStyle = { + gap: unitGap || '1rem', + }; + + const blockProps = useBlockProps.save({ + className: `airo-wp-countdown-timer airo-wp-countdown-timer--${layout}`, + style: containerStyle, + 'data-target-datetime': targetDateTime, + 'data-timezone': timezone, + 'data-show-days': showDays, + 'data-show-hours': showHours, + 'data-show-minutes': showMinutes, + 'data-show-seconds': showSeconds, + 'data-completion-action': completionAction, + 'data-completion-message': completionMessage, + }); + + const units = []; + + if (showDays) { + units.push({ type: 'days', label: 'Days', value: '00' }); + } + + if (showHours) { + units.push({ type: 'hours', label: 'Hours', value: '00' }); + } + + if (showMinutes) { + units.push({ type: 'minutes', label: 'Min', value: '00' }); + } + + if (showSeconds) { + units.push({ type: 'seconds', label: 'Sec', value: '00' }); + } + + return ( +
    +
    + {units.map((unit) => ( +
    +
    + {unit.value} +
    +
    + {unit.label} +
    +
    + ))} +
    +
    + {completionMessage} +
    +
    + ); + }, +}; + +const v2 = { + supports: sharedSupports, + attributes: { + targetDateTime: { + type: 'string', + default: '', + }, + timezone: { + type: 'string', + default: '', + }, + showDays: { + type: 'boolean', + default: true, + }, + showHours: { + type: 'boolean', + default: true, + }, + showMinutes: { + type: 'boolean', + default: true, + }, + showSeconds: { + type: 'boolean', + default: true, + }, + layout: { + type: 'string', + default: 'boxed', + enum: ['boxed', 'inline', 'compact'], + }, + completionAction: { + type: 'string', + default: 'message', + enum: ['message', 'hide'], + }, + completionMessage: { + type: 'string', + default: 'The countdown has ended!', + }, + numberColor: { + type: 'string', + default: '', + }, + labelColor: { + type: 'string', + default: '', + }, + unitBackgroundColor: { + type: 'string', + default: '', + }, + unitBorderColor: { + type: 'string', + default: '', + }, + unitBorderWidth: { + type: 'number', + default: 2, + }, + unitBorderRadius: { + type: 'number', + default: 12, + }, + unitGap: { + type: 'string', + default: '1rem', + }, + unitPadding: { + type: 'string', + default: '1.5rem', + }, + }, + + isEligible(attributes) { + // v2 blocks have unitBorderColor/unitBorderWidth but NOT v1-specific attrs + // v1 blocks also have textAlign, numberFontSize, labelFontSize + return ( + (Object.prototype.hasOwnProperty.call( + attributes, + 'unitBorderColor' + ) || + Object.prototype.hasOwnProperty.call( + attributes, + 'unitBorderWidth' + )) && + !attributes.unitBorder && + !Object.prototype.hasOwnProperty.call(attributes, 'textAlign') && + !Object.prototype.hasOwnProperty.call(attributes, 'numberFontSize') + ); + }, + + migrate(attributes) { + const { unitBorderColor, unitBorderWidth, ...otherAttributes } = + attributes; + + // Convert old attributes to new unitBorder object + return { + ...otherAttributes, + unitBorder: { + color: unitBorderColor || '', + style: 'solid', + width: unitBorderWidth ? `${unitBorderWidth}px` : '2px', + }, + }; + }, + + save({ attributes }) { + const { + targetDateTime, + timezone, + showDays, + showHours, + showMinutes, + showSeconds, + layout, + completionAction, + completionMessage, + numberColor, + labelColor, + unitBackgroundColor, + unitBorderColor, + unitBorderWidth, + unitBorderRadius, + unitGap, + unitPadding, + } = attributes; + + // Build unit styles - use CSS variable for accent-2 fallback + const unitStyle = { + backgroundColor: + convertPresetToCSSVar(unitBackgroundColor) || 'transparent', + borderColor: + convertPresetToCSSVar(unitBorderColor) || + 'var(--wp--preset--color--accent-2, currentColor)', + borderWidth: `${unitBorderWidth}px`, + borderStyle: 'solid', + borderRadius: `${unitBorderRadius}px`, + padding: unitPadding || '1.5rem', + }; + + const numberStyle = { + color: + convertPresetToCSSVar(numberColor) || + 'var(--wp--preset--color--accent-2, currentColor)', + }; + + const labelStyle = { + color: convertPresetToCSSVar(labelColor) || 'currentColor', + }; + + const containerStyle = { + gap: unitGap || '1rem', + }; + + // Create data attributes for frontend JavaScript + const blockProps = useBlockProps.save({ + className: `airo-wp-countdown-timer airo-wp-countdown-timer--${layout}`, + style: containerStyle, + 'data-target-datetime': targetDateTime, + 'data-timezone': timezone, + 'data-show-days': showDays, + 'data-show-hours': showHours, + 'data-show-minutes': showMinutes, + 'data-show-seconds': showSeconds, + 'data-completion-action': completionAction, + 'data-completion-message': completionMessage, + }); + + // Build initial display (will be updated by frontend JS) + const units = []; + + if (showDays) { + units.push({ + type: 'days', + label: 'Days', + value: '00', + }); + } + + if (showHours) { + units.push({ + type: 'hours', + label: 'Hours', + value: '00', + }); + } + + if (showMinutes) { + units.push({ + type: 'minutes', + label: 'Min', + value: '00', + }); + } + + if (showSeconds) { + units.push({ + type: 'seconds', + label: 'Sec', + value: '00', + }); + } + + return ( +
    +
    + {units.map((unit) => ( +
    +
    + {unit.value} +
    +
    + {unit.label} +
    +
    + ))} +
    +
    + {completionMessage} +
    +
    + ); + }, +}; + +/** + * Deprecated version 1: Pre-Block Supports refactoring + * + * This deprecation handles blocks created before we migrated to WordPress Block Supports. + * + * Changes in current version: + * - Removed textAlign attribute (now uses typography.__experimentalTextAlign support) + * - Removed numberFontSize attribute (now uses em units relative to parent fontSize) + * - Removed labelFontSize attribute (now uses em units relative to parent fontSize) + * - Removed unitBorderWidth attribute (now uses __experimentalBorder support) + * - Removed unitBorderRadius attribute (now uses __experimentalBorder support) + */ +const v1 = { + supports: sharedSupports, + attributes: { + targetDateTime: { + type: 'string', + default: '', + }, + timezone: { + type: 'string', + default: '', + }, + showDays: { + type: 'boolean', + default: true, + }, + showHours: { + type: 'boolean', + default: true, + }, + showMinutes: { + type: 'boolean', + default: true, + }, + showSeconds: { + type: 'boolean', + default: true, + }, + layout: { + type: 'string', + default: 'boxed', + enum: ['boxed', 'inline', 'compact'], + }, + textAlign: { + type: 'string', + default: 'center', + }, + completionAction: { + type: 'string', + default: 'message', + enum: ['message', 'hide'], + }, + completionMessage: { + type: 'string', + default: 'The countdown has ended!', + }, + numberColor: { + type: 'string', + default: '', + }, + labelColor: { + type: 'string', + default: '', + }, + unitBackgroundColor: { + type: 'string', + default: '', + }, + unitBorderColor: { + type: 'string', + default: '', + }, + unitBorderWidth: { + type: 'number', + default: 2, + }, + unitBorderRadius: { + type: 'number', + default: 12, + }, + unitGap: { + type: 'string', + default: '1rem', + }, + unitPadding: { + type: 'string', + default: '1.5rem', + }, + numberFontSize: { + type: 'string', + default: '3rem', + }, + labelFontSize: { + type: 'string', + default: '1rem', + }, + }, + + isEligible(attributes) { + // v1 blocks have textAlign, numberFontSize, labelFontSize attributes + return ( + Object.prototype.hasOwnProperty.call(attributes, 'textAlign') || + Object.prototype.hasOwnProperty.call( + attributes, + 'numberFontSize' + ) || + Object.prototype.hasOwnProperty.call(attributes, 'labelFontSize') + ); + }, + + save({ attributes }) { + const { + targetDateTime, + timezone, + showDays, + showHours, + showMinutes, + showSeconds, + layout, + textAlign, + completionAction, + completionMessage, + numberColor, + labelColor, + unitBackgroundColor, + unitBorderColor, + unitBorderWidth, + unitBorderRadius, + unitGap, + unitPadding, + numberFontSize, + labelFontSize, + } = attributes; + + // Build unit styles - use CSS variable for accent-2 fallback + const unitStyle = { + backgroundColor: + convertPresetToCSSVar(unitBackgroundColor) || 'transparent', + borderColor: + convertPresetToCSSVar(unitBorderColor) || + 'var(--wp--preset--color--accent-2, currentColor)', + borderWidth: `${unitBorderWidth}px`, + borderStyle: 'solid', + borderRadius: `${unitBorderRadius}px`, + padding: unitPadding || '1.5rem', + }; + + const numberStyle = { + color: + convertPresetToCSSVar(numberColor) || + 'var(--wp--preset--color--accent-2, currentColor)', + fontSize: numberFontSize || '3rem', + }; + + const labelStyle = { + color: convertPresetToCSSVar(labelColor) || 'currentColor', + fontSize: labelFontSize || '1rem', + }; + + // Calculate text alignment + let justifyValue = 'center'; + if (textAlign === 'left') { + justifyValue = 'flex-start'; + } else if (textAlign === 'right') { + justifyValue = 'flex-end'; + } + + const containerStyle = { + gap: unitGap || '1rem', + justifyContent: justifyValue, + }; + + // Create data attributes for frontend JavaScript + const blockProps = useBlockProps.save({ + className: `airo-wp-countdown-timer airo-wp-countdown-timer--${layout}`, + style: containerStyle, + 'data-target-datetime': targetDateTime, + 'data-timezone': timezone, + 'data-show-days': showDays, + 'data-show-hours': showHours, + 'data-show-minutes': showMinutes, + 'data-show-seconds': showSeconds, + 'data-completion-action': completionAction, + 'data-completion-message': completionMessage, + }); + + // Build initial display (will be updated by frontend JS) + const units = []; + + if (showDays) { + units.push({ + type: 'days', + label: 'Days', + value: '00', + }); + } + + if (showHours) { + units.push({ + type: 'hours', + label: 'Hours', + value: '00', + }); + } + + if (showMinutes) { + units.push({ + type: 'minutes', + label: 'Min', + value: '00', + }); + } + + if (showSeconds) { + units.push({ + type: 'seconds', + label: 'Sec', + value: '00', + }); + } + + return ( +
    +
    + {units.map((unit) => ( +
    +
    + {unit.value} +
    +
    + {unit.label} +
    +
    + ))} +
    +
    + {completionMessage} +
    +
    + ); + }, + + migrate(attributes) { + const { + textAlign, + numberFontSize, + labelFontSize, + unitBorderColor, + unitBorderWidth, + ...otherAttributes + } = attributes; + + return { + ...otherAttributes, + unitBorder: { + color: unitBorderColor || '', + style: 'solid', + width: unitBorderWidth ? `${unitBorderWidth}px` : '2px', + }, + }; + }, +}; + +export { v3, v2, v1 }; + +export default [v3, v2, v1]; diff --git a/src/blocks/countdown-timer/edit.js b/src/blocks/countdown-timer/edit.js new file mode 100644 index 0000000..76e9d81 --- /dev/null +++ b/src/blocks/countdown-timer/edit.js @@ -0,0 +1,332 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + InspectorControls, + useSettings, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { Placeholder } from '@wordpress/components'; +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { DsgoInspectorPanel } from '../../components/shared'; +import DateTimePanel from './components/inspector/DateTimePanel'; +import DisplayPanel from './components/inspector/DisplayPanel'; +import StylingPanel from './components/inspector/StylingPanel'; +import UnitBorderPanel, { + DEFAULT_UNIT_BORDER, +} from './components/inspector/UnitBorderPanel'; +import CompletionPanel from './components/inspector/CompletionPanel'; +import { + calculateTimeRemaining, + formatTimeUnit, +} from './utils/time-calculator'; +import { formatCountdownDisplay } from './utils/format-time'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const DEFAULT_COMPLETION_MESSAGE = 'The countdown has ended!'; + +/** + * Edit component for Countdown Timer block + * + * @param {Object} props - Block properties + * @return {JSX.Element} Edit component + */ +export default function Edit(props) { + const { attributes, setAttributes, clientId } = props; + const { + targetDateTime, + timezone, + showDays, + showHours, + showMinutes, + showSeconds, + layout, + completionAction, + completionMessage, + numberColor, + labelColor, + unitBackgroundColor, + unitBorder, + unitBorderRadius, + unitGap, + unitPadding, + } = attributes; + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Get theme color palette (WordPress 6.5+ - useSettings returns array) + const [colorSettings] = useSettings('color.palette'); + + // Get theme accent-2 color as default (no hardcoded fallback) + const themeColors = colorSettings?.theme || []; + const accent2Color = themeColors.find((color) => color.slug === 'accent-2'); + const defaultAccentColor = accent2Color?.color || ''; + + // State for live countdown preview in editor + const [currentTime, setCurrentTime] = useState( + calculateTimeRemaining(targetDateTime, timezone) + ); + + // Set default date to 7 days from now on first load + useEffect(() => { + if (!targetDateTime) { + const sevenDaysFromNow = new Date(); + sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7); + setAttributes({ targetDateTime: sevenDaysFromNow.toISOString() }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Empty dependency array = run only once on mount + + // Update countdown every second in editor + useEffect(() => { + if (!targetDateTime) { + return; + } + + const interval = setInterval(() => { + setCurrentTime(calculateTimeRemaining(targetDateTime, timezone)); + }, 1000); + + return () => clearInterval(interval); + }, [targetDateTime, timezone]); + + // Build unit styles - use accent-2 if available, otherwise currentColor + const unitStyle = { + backgroundColor: + convertColorToCSSVar(unitBackgroundColor) || 'transparent', + borderColor: unitBorder?.color || defaultAccentColor || 'currentColor', + borderWidth: unitBorder?.width || '2px', + borderStyle: unitBorder?.style || 'solid', + borderRadius: `${unitBorderRadius}px`, + padding: unitPadding || '1.5rem', + }; + + const numberStyle = { + color: + convertColorToCSSVar(numberColor) || + defaultAccentColor || + 'currentColor', + }; + + const labelStyle = { + color: convertColorToCSSVar(labelColor) || 'currentColor', + }; + + const containerStyle = { + gap: unitGap || '1rem', + }; + + const blockProps = useBlockProps({ + className: `airo-wp-countdown-timer airo-wp-countdown-timer--${layout}`, + style: containerStyle, + }); + + // Format units for display + const visibleUnits = formatCountdownDisplay(currentTime, { + showDays, + showHours, + showMinutes, + showSeconds, + }); + + // Extracted inspector — rendered once per branch below. Composing all + // sub-panels under one DsgoInspectorPanel follows the plugin-wide + // Settings convention. Color dropdown stays in the native "color" + // group so it slots into WP's Color panel. + const inspector = ( + <> + + + setAttributes({ + numberColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Label Color', 'airo-wp'), + colorValue: decodeColorValue( + labelColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + labelColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Unit Background', 'airo-wp'), + colorValue: decodeColorValue( + unitBackgroundColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + unitBackgroundColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + + + + setAttributes({ + targetDateTime: '', + timezone: '', + showDays: true, + showHours: true, + showMinutes: true, + showSeconds: true, + layout: 'boxed', + completionAction: 'message', + completionMessage: DEFAULT_COMPLETION_MESSAGE, + unitGap: '1rem', + unitPadding: '1.5rem', + unitBorder: DEFAULT_UNIT_BORDER, + unitBorderRadius: 12, + }) + } + > + + + + + + + + + ); + + // Show placeholder if no target date is set + if (!targetDateTime) { + return ( +
    + {inspector} + +
    + ); + } + + // Show completion state — hidden timer + if (currentTime.isComplete && completionAction === 'hide') { + return ( +
    + {inspector} +
    +

    + {__('Timer hidden (countdown complete)', 'airo-wp')} +

    +
    +
    + ); + } + + // Show completion state — custom message + if (currentTime.isComplete) { + return ( +
    + {inspector} +
    + {completionMessage} +
    +
    + ); + } + + // Show countdown timer + return ( +
    + {inspector} +
    + {visibleUnits.map((unit) => ( +
    +
    + {formatTimeUnit(unit.value)} +
    +
    + {unit.label} +
    +
    + ))} +
    +
    + ); +} diff --git a/src/blocks/countdown-timer/editor.scss b/src/blocks/countdown-timer/editor.scss new file mode 100644 index 0000000..c588830 --- /dev/null +++ b/src/blocks/countdown-timer/editor.scss @@ -0,0 +1,263 @@ +/** + * Editor-specific styles for Countdown Timer block + * CRITICAL: Must be duplicated from style.scss for editor/frontend parity + */ + +// Fix DateTimePicker horizontal alignment and overflow issues +.components-datetime { + + .components-datetime__time { + display: flex; + align-items: flex-end; // Align to bottom for consistent baseline + flex-wrap: wrap; + gap: 8px; + max-width: 100%; + + .components-datetime__time-wrapper { + display: flex; + align-items: flex-end; // Align inputs to bottom + gap: 4px; + flex-wrap: wrap; + } + + .components-datetime__time-field-hours, + .components-datetime__time-field-minutes, + .components-datetime__time-field-am-pm { + max-width: 60px; + + fieldset { + margin-bottom: 0 !important; // Remove bottom margin + } + + // Override WordPress core margin-bottom on BaseControls + .components-base-control { + margin-bottom: 0 !important; + } + } + + .components-datetime__time-field-am-pm { + display: inline-flex; + align-items: flex-end; + + // Override WordPress core margin-bottom on BaseControls + .components-base-control { + margin-bottom: 0 !important; + } + } + + .components-datetime__timezone { + flex-shrink: 0; + font-size: 12px; + align-self: flex-end; // Align timezone to bottom + padding-bottom: 2px; // Small adjustment for baseline + margin-bottom: 0 !important; + } + + fieldset { + margin-bottom: 0; + } + + input { + max-width: 100%; + } + + // Catch-all to override WordPress core margin-bottom + .components-base-control { + margin-bottom: 0 !important; + } + } + + .components-datetime__date { + + .components-datetime__date-wrapper { + display: flex; + align-items: flex-end; // Align date fields to bottom + gap: 8px; + flex-wrap: wrap; + + > * { + margin-bottom: 0 !important; + } + + fieldset { + margin-bottom: 0 !important; + } + + // Override WordPress core margin-bottom on BaseControls + .components-base-control { + margin-bottom: 0 !important; + } + } + } + + // Global override for all BaseControls within DateTimePicker + .components-base-control, + .components-base-control__field { + margin-bottom: 0 !important; + } +} + +.airo-wp-countdown-timer { + display: flex; + // Default to center, but allow text-align to override + justify-content: center; + align-items: center; + max-width: 100%; // Prevent overflow in containers and on small screens + + // Text alignment support - respect WordPress textAlign attribute + &.has-text-align-left { + justify-content: flex-start; + } + + &.has-text-align-center { + justify-content: center; + } + + &.has-text-align-right { + justify-content: flex-end; + } + + // Wide alignment - use negative margins to extend to wide width + &.alignwide { + width: var(--wp--style--global--wide-size); + max-width: 100%; + } + + // Full alignment - use negative margins to extend edge-to-edge + &.alignfull { + width: 100%; + max-width: 100%; + margin-right: calc(var(--wp--style--root--padding-right) * -1); + margin-left: calc(var(--wp--style--root--padding-left) * -1); + } + + &__units { + display: flex; + flex-wrap: wrap; + // Inherit alignment from parent + justify-content: inherit; + align-items: center; + } + + &__unit { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + min-width: 80px; + box-sizing: border-box; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-2px); + } + } + + &__number { + font-weight: 700; + line-height: 1.2; + margin-bottom: 0.25rem; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + // Use em units relative to parent fontSize (from Block Supports) + // Default: 3em (3x the block's fontSize) + font-size: 3em; + } + + &__label { + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1; + opacity: 0.8; + // Use em units relative to parent fontSize (from Block Supports) + // Default: 1em (same as block's fontSize) + font-size: 1em; + } + + &__completion-message { + text-align: center; + font-size: 1.5rem; + font-weight: 600; + padding: 2rem; + } + + // Boxed Layout (Default) + &--boxed { + + .airo-wp-countdown-timer__units { + gap: inherit; + } + + .airo-wp-countdown-timer__unit { + min-width: 100px; + padding: 1.5rem; + } + } + + // Inline Layout + &--inline { + + .airo-wp-countdown-timer__units { + gap: 0.5rem; + flex-wrap: nowrap; + } + + .airo-wp-countdown-timer__unit { + flex-direction: row; + min-width: auto; + padding: 0.5rem 1rem; + gap: 0.5rem; + border-width: 1px; + } + + .airo-wp-countdown-timer__number { + margin-bottom: 0; + font-size: 1.5em; + } + + .airo-wp-countdown-timer__label { + font-size: 0.875em; + opacity: 0.7; + } + + .airo-wp-countdown-timer__unit:not(:last-child)::after { + content: ':'; + margin-left: 0.5rem; + opacity: 0.5; + } + } + + // Compact Layout + &--compact { + + .airo-wp-countdown-timer__units { + gap: 0.25rem; + } + + .airo-wp-countdown-timer__unit { + min-width: 60px; + padding: 0.75rem; + border-width: 1px; + } + + .airo-wp-countdown-timer__number { + font-size: 1.75em; + margin-bottom: 0.125rem; + } + + .airo-wp-countdown-timer__label { + font-size: 0.75em; + } + } + + // Editor-specific: Add visual feedback for completion state + &__completion { + padding: 2rem; + text-align: center; + border: 2px dashed #ccc; + border-radius: 8px; + background: #f9f9f9; + } +} diff --git a/src/blocks/countdown-timer/index.js b/src/blocks/countdown-timer/index.js new file mode 100644 index 0000000..19fb94b --- /dev/null +++ b/src/blocks/countdown-timer/index.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import edit from './edit'; +import save from './save'; +import deprecated from './deprecated'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +/** + * Register the Countdown Timer block + */ +registerBlockType(metadata.name, { + ...metadata, + edit, + save, + deprecated, + icon: { + src: ( + + + + + ), + foreground: ICON_COLOR, + }, +}); diff --git a/src/blocks/countdown-timer/save.js b/src/blocks/countdown-timer/save.js new file mode 100644 index 0000000..be91278 --- /dev/null +++ b/src/blocks/countdown-timer/save.js @@ -0,0 +1,146 @@ +/** + * WordPress dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +/** + * Save component for Countdown Timer block + * + * @param {Object} props - Block properties + * @param {Object} props.attributes - Block attributes + * @return {JSX.Element} Save component + */ +export default function save({ attributes }) { + const { + targetDateTime, + timezone, + showDays, + showHours, + showMinutes, + showSeconds, + layout, + completionAction, + completionMessage, + numberColor, + labelColor, + unitBackgroundColor, + unitBorder, + unitBorderRadius, + unitGap, + unitPadding, + } = attributes; + + // Build unit styles - use CSS variable for accent-2 fallback + const unitStyle = { + backgroundColor: + convertColorToCSSVar(unitBackgroundColor) || 'transparent', + borderColor: + unitBorder?.color || + 'var(--wp--preset--color--accent-2, currentColor)', + borderWidth: unitBorder?.width || '2px', + borderStyle: unitBorder?.style || 'solid', + borderRadius: `${unitBorderRadius}px`, + padding: unitPadding || '1.5rem', + }; + + const numberStyle = { + color: + convertColorToCSSVar(numberColor) || + 'var(--wp--preset--color--accent-2, currentColor)', + }; + + const labelStyle = { + color: convertColorToCSSVar(labelColor) || 'currentColor', + }; + + const containerStyle = { + gap: unitGap || '1rem', + }; + + // Create data attributes for frontend JavaScript + const blockProps = useBlockProps.save({ + className: `airo-wp-countdown-timer airo-wp-countdown-timer--${layout}`, + style: containerStyle, + 'data-target-datetime': targetDateTime, + 'data-timezone': timezone, + 'data-show-days': showDays, + 'data-show-hours': showHours, + 'data-show-minutes': showMinutes, + 'data-show-seconds': showSeconds, + 'data-completion-action': completionAction, + 'data-completion-message': completionMessage, + }); + + // Build initial display (will be updated by frontend JS) + const units = []; + + if (showDays) { + units.push({ + type: 'days', + label: 'Days', + value: '00', + }); + } + + if (showHours) { + units.push({ + type: 'hours', + label: 'Hours', + value: '00', + }); + } + + if (showMinutes) { + units.push({ + type: 'minutes', + label: 'Min', + value: '00', + }); + } + + if (showSeconds) { + units.push({ + type: 'seconds', + label: 'Sec', + value: '00', + }); + } + + return ( +
    +
    + {units.map((unit) => ( +
    +
    + {unit.value} +
    +
    + {unit.label} +
    +
    + ))} +
    + {/* + * Hidden by default via style.scss + * (`.airo-wp-countdown-timer__completion-message { display: none }`), + * not an inline style — view.js reveals it by setting + * `style.display = 'block'`, which beats the stylesheet rule anyway. + */} +
    + {completionMessage} +
    +
    + ); +} diff --git a/src/blocks/countdown-timer/style.scss b/src/blocks/countdown-timer/style.scss new file mode 100644 index 0000000..7b765ea --- /dev/null +++ b/src/blocks/countdown-timer/style.scss @@ -0,0 +1,244 @@ +/** + * Frontend styles for Countdown Timer block + * CRITICAL: Must be duplicated from editor.scss for editor/frontend parity + */ + +.airo-wp-countdown-timer { + display: flex; + // Default to center, but allow text-align to override + justify-content: center; + align-items: center; + max-width: 100%; // Prevent overflow in containers and on small screens + + // Text alignment support - respect WordPress textAlign attribute + &.has-text-align-left { + justify-content: flex-start; + } + + &.has-text-align-center { + justify-content: center; + } + + &.has-text-align-right { + justify-content: flex-end; + } + + // Wide alignment - use negative margins to extend to wide width + &.alignwide { + width: var(--wp--style--global--wide-size); + max-width: 100%; + } + + // Full alignment - use negative margins to extend edge-to-edge + &.alignfull { + width: 100%; + max-width: 100%; + margin-right: calc(var(--wp--style--root--padding-right) * -1); + margin-left: calc(var(--wp--style--root--padding-left) * -1); + } + + &__units { + display: flex; + flex-wrap: wrap; + // Inherit alignment from parent + justify-content: inherit; + align-items: center; + } + + &__unit { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + min-width: 80px; + box-sizing: border-box; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-2px); + } + } + + &__number { + font-weight: 700; + line-height: 1.2; + margin-bottom: 0.25rem; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + // Use em units relative to parent fontSize (from Block Supports) + // Default: 3em (3x the block's fontSize) + font-size: 3em; + } + + &__label { + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1; + opacity: 0.8; + // Use em units relative to parent fontSize (from Block Supports) + // Default: 1em (same as block's fontSize) + font-size: 1em; + } + + &__completion-message { + text-align: center; + font-size: 1.5rem; + font-weight: 600; + padding: 2rem; + display: none; + } + + // Boxed Layout (Default) + &--boxed { + + .airo-wp-countdown-timer__units { + gap: inherit; + } + + .airo-wp-countdown-timer__unit { + min-width: 100px; + padding: 1.5rem; + } + } + + // Inline Layout + &--inline { + + .airo-wp-countdown-timer__units { + gap: 0.5rem; + flex-wrap: nowrap; + } + + .airo-wp-countdown-timer__unit { + flex-direction: row; + min-width: auto; + padding: 0.5rem 1rem; + gap: 0.5rem; + border-width: 1px; + } + + .airo-wp-countdown-timer__number { + margin-bottom: 0; + font-size: 1.5em; + } + + .airo-wp-countdown-timer__label { + font-size: 0.875em; + opacity: 0.7; + } + + .airo-wp-countdown-timer__unit:not(:last-child)::after { + content: ':'; + margin-left: 0.5rem; + opacity: 0.5; + } + } + + // Compact Layout + &--compact { + + .airo-wp-countdown-timer__units { + gap: 0.25rem; + } + + .airo-wp-countdown-timer__unit { + min-width: 60px; + padding: 0.75rem; + border-width: 1px; + } + + .airo-wp-countdown-timer__number { + font-size: 1.75em; + margin-bottom: 0.125rem; + } + + .airo-wp-countdown-timer__label { + font-size: 0.75em; + } + } + + // Responsive adjustments + @media (max-width: 768px) { + + &--boxed { + + .airo-wp-countdown-timer__unit { + min-width: 80px; + padding: 1rem; + } + + .airo-wp-countdown-timer__number { + font-size: 2em; + } + + .airo-wp-countdown-timer__label { + font-size: 0.875em; + } + } + + &--inline { + + .airo-wp-countdown-timer__units { + flex-wrap: wrap; + } + } + } + + @media (max-width: 480px) { + + &--boxed { + + .airo-wp-countdown-timer__units { + gap: 0.5rem; + } + + .airo-wp-countdown-timer__unit { + min-width: 70px; + padding: 0.75rem; + } + + .airo-wp-countdown-timer__number { + font-size: 1.5em; + } + + .airo-wp-countdown-timer__label { + font-size: 0.75em; + } + } + + &--compact { + + .airo-wp-countdown-timer__unit { + min-width: 50px; + padding: 0.5rem; + } + + .airo-wp-countdown-timer__number { + font-size: 1.25em; + } + } + } + + // Accessibility: Respect reduced motion preference + @media (prefers-reduced-motion: reduce) { + + .airo-wp-countdown-timer__unit { + transition: none; + + &:hover { + transform: none; + } + } + } + + // Print styles + @media print { + + .airo-wp-countdown-timer__unit { + border: 1px solid #000 !important; + break-inside: avoid; + } + } +} diff --git a/src/blocks/countdown-timer/utils/format-time.js b/src/blocks/countdown-timer/utils/format-time.js new file mode 100644 index 0000000..6c65462 --- /dev/null +++ b/src/blocks/countdown-timer/utils/format-time.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Format countdown time for display + * + * @param {Object} timeData - Object with days, hours, minutes, seconds + * @param {Object} visibilitySettings - Object with showDays, showHours, showMinutes, showSeconds + * @return {Array} Array of visible time units with labels + */ +export function formatCountdownDisplay(timeData, visibilitySettings) { + const units = []; + + if (visibilitySettings.showDays) { + units.push({ + value: timeData.days, + label: + timeData.days === 1 + ? __('Day', 'airo-wp') + : __('Days', 'airo-wp'), + type: 'days', + }); + } + + if (visibilitySettings.showHours) { + units.push({ + value: timeData.hours, + label: + timeData.hours === 1 + ? __('Hour', 'airo-wp') + : __('Hours', 'airo-wp'), + type: 'hours', + }); + } + + if (visibilitySettings.showMinutes) { + units.push({ + value: timeData.minutes, + label: __('Min', 'airo-wp'), + type: 'minutes', + }); + } + + if (visibilitySettings.showSeconds) { + units.push({ + value: timeData.seconds, + label: __('Sec', 'airo-wp'), + type: 'seconds', + }); + } + + return units; +} + +/** + * Get unit label (singular or plural) + * + * @param {string} unitType - Type of unit (days, hours, minutes, seconds) + * @param {number} value - Value of the unit + * @return {string} Label for the unit + */ +export function getUnitLabel(unitType, value) { + const labels = { + days: + value === 1 ? __('Day', 'airo-wp') : __('Days', 'airo-wp'), + hours: + value === 1 + ? __('Hour', 'airo-wp') + : __('Hours', 'airo-wp'), + minutes: __('Min', 'airo-wp'), + seconds: __('Sec', 'airo-wp'), + }; + + return labels[unitType] || ''; +} diff --git a/src/blocks/countdown-timer/utils/time-calculator.js b/src/blocks/countdown-timer/utils/time-calculator.js new file mode 100644 index 0000000..b47b89b --- /dev/null +++ b/src/blocks/countdown-timer/utils/time-calculator.js @@ -0,0 +1,103 @@ +/** + * Calculate the time remaining until a target date/time + * + * @param {string} targetDateTime - ISO 8601 datetime string + * @param {string} timezone - IANA timezone string (reserved for future use) + * @return {Object} Object containing days, hours, minutes, seconds, and isComplete flag + */ +export function calculateTimeRemaining(targetDateTime, timezone = '') { + // Note: timezone parameter reserved for future timezone-aware calculations + // eslint-disable-next-line no-unused-vars + const _ = timezone; + if (!targetDateTime) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isComplete: true, + }; + } + + try { + // Parse the target datetime + const targetDate = new Date(targetDateTime); + + // Get current time + const now = new Date(); + + // Calculate the difference in milliseconds + const difference = targetDate.getTime() - now.getTime(); + + // Check if countdown is complete + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isComplete: true, + }; + } + + // Calculate time units + const days = Math.floor(difference / (1000 * 60 * 60 * 24)); + const hours = Math.floor( + (difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) + ); + const minutes = Math.floor( + (difference % (1000 * 60 * 60)) / (1000 * 60) + ); + const seconds = Math.floor((difference % (1000 * 60)) / 1000); + + return { + days, + hours, + minutes, + seconds, + isComplete: false, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error calculating time remaining:', error); + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isComplete: true, + }; + } +} + +/** + * Format a time unit value with leading zero if needed + * + * @param {number} value - The time unit value + * @return {string} Formatted value with leading zero + */ +export function formatTimeUnit(value) { + return value < 10 ? `0${value}` : `${value}`; +} + +/** + * Get a human-readable timezone offset + * + * @param {string} timezone - IANA timezone string + * @return {string} Timezone offset (e.g., 'UTC-5') + */ +export function getTimezoneOffset(timezone) { + try { + const date = new Date(); + const options = { + timeZone: timezone, + timeZoneName: 'short', + }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + const timeZonePart = parts.find((part) => part.type === 'timeZoneName'); + return timeZonePart ? timeZonePart.value : ''; + } catch (error) { + return ''; + } +} diff --git a/src/blocks/countdown-timer/view.js b/src/blocks/countdown-timer/view.js new file mode 100644 index 0000000..239faf8 --- /dev/null +++ b/src/blocks/countdown-timer/view.js @@ -0,0 +1,227 @@ +/** + * Frontend JavaScript for Countdown Timer block + */ + +/** + * Calculate time remaining until target date + * + * @param {string} targetDateTime - ISO 8601 datetime string + * @return {Object} Object with days, hours, minutes, seconds, isComplete + */ +function calculateTimeRemaining(targetDateTime) { + if (!targetDateTime) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isComplete: true, + }; + } + + const targetDate = new Date(targetDateTime); + const now = new Date(); + const difference = targetDate.getTime() - now.getTime(); + + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isComplete: true, + }; + } + + const days = Math.floor(difference / (1000 * 60 * 60 * 24)); + const hours = Math.floor( + (difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) + ); + const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((difference % (1000 * 60)) / 1000); + + return { + days, + hours, + minutes, + seconds, + isComplete: false, + }; +} + +/** + * Format time unit with leading zero + * + * @param {number} value - Time unit value + * @return {string} Formatted value + */ +function formatTimeUnit(value) { + return value < 10 ? `0${value}` : `${value}`; +} + +/** + * Get unit label (singular/plural) + * + * @param {string} unitType - Type of unit + * @param {number} value - Current value + * @return {string} Label text + */ +function getUnitLabel(unitType, value) { + const labels = { + days: value === 1 ? 'Day' : 'Days', + hours: value === 1 ? 'Hour' : 'Hours', + minutes: 'Min', + seconds: 'Sec', + }; + return labels[unitType] || ''; +} + +/** + * Update countdown display + * + * @param {Element} timer - Timer element + * @param {Object} timeData - Time data object + */ +function updateCountdownDisplay(timer, timeData) { + const units = timer.querySelectorAll('.airo-wp-countdown-timer__unit'); + + units.forEach((unit) => { + const unitType = unit.dataset.unitType; + const numberElement = unit.querySelector( + '.airo-wp-countdown-timer__number' + ); + const labelElement = unit.querySelector('.airo-wp-countdown-timer__label'); + + if (numberElement && timeData[unitType] !== undefined) { + numberElement.textContent = formatTimeUnit(timeData[unitType]); + } + + if (labelElement && timeData[unitType] !== undefined) { + labelElement.textContent = getUnitLabel( + unitType, + timeData[unitType] + ); + } + }); +} + +/** + * Handle countdown completion + * + * @param {Element} timer - Timer element + */ +function handleCompletion(timer) { + const completionAction = timer.dataset.completionAction; + const completionMessage = timer.dataset.completionMessage; + + if (completionAction === 'hide') { + // Hide the entire timer + timer.style.display = 'none'; + } else if (completionAction === 'message') { + // Hide units and show completion message + const unitsContainer = timer.querySelector( + '.airo-wp-countdown-timer__units' + ); + const messageContainer = timer.querySelector( + '.airo-wp-countdown-timer__completion-message' + ); + + if (unitsContainer) { + unitsContainer.style.display = 'none'; + } + + if (messageContainer) { + messageContainer.style.display = 'block'; + messageContainer.textContent = completionMessage; + } + } +} + +/** + * Initialize a countdown timer + * + * @param {Element} timer - Timer element + */ +function initCountdownTimer(timer) { + const targetDateTime = timer.dataset.targetDatetime; + + if (!targetDateTime) { + return; + } + + // Check for reduced motion preference + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; + + // Initial update + let timeData = calculateTimeRemaining(targetDateTime); + updateCountdownDisplay(timer, timeData); + + if (timeData.isComplete) { + handleCompletion(timer); + return; + } + + // Update every second + const interval = setInterval( + () => { + timeData = calculateTimeRemaining(targetDateTime); + updateCountdownDisplay(timer, timeData); + + if (timeData.isComplete) { + clearInterval(interval); + handleCompletion(timer); + } + }, + prefersReducedMotion ? 5000 : 1000 + ); // Slower updates for reduced motion + + // Clean up on page unload + window.addEventListener('beforeunload', () => { + clearInterval(interval); + }); +} + +/** + * Initialize all countdown timers on the page + */ +function initAllCountdownTimers() { + const timers = document.querySelectorAll('.airo-wp-countdown-timer'); + + timers.forEach((timer) => { + // Prevent duplicate initialization (critical for bfcache — avoids setInterval leaks) + if (timer.hasAttribute('data-airo-wp-initialized')) { + return; + } + timer.setAttribute('data-airo-wp-initialized', 'true'); + + // Use Intersection Observer for lazy initialization + // eslint-disable-next-line no-undef + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + initCountdownTimer(entry.target); + observer.unobserve(entry.target); + } + }); + }, + { + rootMargin: '50px', // Start 50px before entering viewport + } + ); + + observer.observe(timer); + }); +} + +// Initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initAllCountdownTimers); +} else { + initAllCountdownTimers(); +} + +// Re-initialize after soft navigation (bfcache, AJAX) +document.addEventListener('airo-wp-content-loaded', initAllCountdownTimers); diff --git a/src/blocks/counter-group/block.json b/src/blocks/counter-group/block.json new file mode 100644 index 0000000..b338946 --- /dev/null +++ b/src/blocks/counter-group/block.json @@ -0,0 +1,143 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/counter-group", + "version": "1.0.0", + "title": "Counter Group", + "category": "airo-wp", + "description": "Display animated statistics and numbers that count up on scroll", + "keywords": [ + "counter", + "stats", + "statistics", + "numbers", + "animated" + ], + "textdomain": "airo-wp", + "icon": "chart-area", + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "inserter": true, + "spacing": { + "margin": true, + "padding": true, + "blockGap": false, + "__experimentalDefaultControls": { + "padding": true + } + }, + "color": { + "background": true, + "text": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "attributes": { + "columns": { + "type": "number", + "default": 3 + }, + "columnsTablet": { + "type": "number", + "default": 2 + }, + "columnsMobile": { + "type": "number", + "default": 1 + }, + "gap": { + "type": "string", + "default": "32px" + }, + "alignContent": { + "type": "string", + "default": "center" + }, + "animationDuration": { + "type": "number", + "default": 2 + }, + "animationDelay": { + "type": "number", + "default": 0 + }, + "animationEasing": { + "type": "string", + "default": "easeOutQuad" + }, + "useGrouping": { + "type": "boolean", + "default": true + }, + "separator": { + "type": "string", + "default": "," + }, + "decimal": { + "type": "string", + "default": "." + }, + "hoverColor": { + "type": "string", + "default": "" + } + }, + "providesContext": { + "airo-wp/counterGroup/animationDuration": "animationDuration", + "airo-wp/counterGroup/animationDelay": "animationDelay", + "airo-wp/counterGroup/animationEasing": "animationEasing", + "airo-wp/counterGroup/useGrouping": "useGrouping", + "airo-wp/counterGroup/separator": "separator", + "airo-wp/counterGroup/decimal": "decimal", + "airo-wp/counterGroup/hoverColor": "hoverColor" + }, + "example": { + "innerBlocks": [ + { + "name": "airo-wp/counter", + "attributes": { + "endValue": 500, + "suffix": "+", + "label": "Happy Customers" + } + }, + { + "name": "airo-wp/counter", + "attributes": { + "prefix": "$", + "endValue": 1000000, + "suffix": "+", + "label": "Revenue Generated" + } + }, + { + "name": "airo-wp/counter", + "attributes": { + "endValue": 99.9, + "decimals": 1, + "suffix": "%", + "label": "Uptime" + } + } + ] + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "viewScript": "file:./view.js" +} diff --git a/src/blocks/counter-group/editor.scss b/src/blocks/counter-group/editor.scss new file mode 100644 index 0000000..8ea48e0 --- /dev/null +++ b/src/blocks/counter-group/editor.scss @@ -0,0 +1,96 @@ +/** + * Counter Group Block - Editor Styles + * + * Editor-specific styles (matches frontend for WYSIWYG) + */ + +.airo-wp-counter-group { + box-sizing: border-box; + max-width: 100%; // Prevent overflow in containers and on small screens + // CRITICAL: Must be duplicated from style.scss for editor/frontend parity + + // Wide alignment - use negative margins to extend to wide width + &.alignwide { + width: var(--wp--style--global--wide-size); + max-width: 100%; + } + + // Full alignment - use negative margins to extend edge-to-edge + &.alignfull { + width: 100%; + max-width: 100%; + margin-right: calc(var(--wp--style--root--padding-right) * -1); + margin-left: calc(var(--wp--style--root--padding-left) * -1); + } + + // Default text color that child Counter blocks will inherit + // WordPress applies inline style when user chooses a color, which takes precedence + color: var(--wp--preset--color--contrast); + + &__inner { + display: grid; + grid-template-columns: repeat(var(--airo-wp-counter-columns-desktop, 3), 1fr); + gap: var(--airo-wp-counter-gap, 32px); + width: 100%; + box-sizing: border-box; + pointer-events: auto; + min-height: 50px; // Ensure there's a clickable area even with few items + + // Alignment classes + &--align-left { + justify-items: start; + text-align: left; + } + + &--align-center { + justify-items: center; + text-align: center; + } + + &--align-right { + justify-items: end; + text-align: right; + } + + // Editor-specific styles to improve UX + // WordPress wraps inner blocks in .block-editor-block-list__layout + // We need to make it transparent so grid applies to the actual counter blocks + > .block-editor-block-list__layout { + display: contents; + } + + // Add a subtle visual indicator when the counter-group is selected + .is-selected > & { + outline: 1px solid var(--wp-admin-theme-color, #007cba); + outline-offset: 2px; + } + } + + // Counter blocks should also be clickable for individual selection + .wp-block-airo-wp-counter { + pointer-events: auto; + } + + // Placeholder state (no inner blocks yet) + .block-editor-inner-blocks.block-editor-block-list__layout:empty::before { + content: attr(data-placeholder); + color: #757575; + font-style: italic; + } + + // Tablet breakpoint (768px - 1023px) + @media (max-width: 1023px) { + + &__inner { + grid-template-columns: repeat(var(--airo-wp-counter-columns-tablet, 2), 1fr); + } + } + + // Mobile breakpoint (<768px) + @media (max-width: 767px) { + + &__inner { + grid-template-columns: repeat(var(--airo-wp-counter-columns-mobile, 1), 1fr); + } + } +} diff --git a/src/blocks/counter-group/index.js b/src/blocks/counter-group/index.js new file mode 100644 index 0000000..1d501cd --- /dev/null +++ b/src/blocks/counter-group/index.js @@ -0,0 +1,487 @@ +/** + * Counter Group Block - Edit Component + * + * Parent block that contains individual Counter blocks + * Provides layout controls and global animation settings + */ + +import { __ } from '@wordpress/i18n'; +import { registerBlockType } from '@wordpress/blocks'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { + RangeControl, + SelectControl, + ToggleControl, + TextControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; + +import metadata from './block.json'; +import save from './save'; +import { ICON_COLOR } from '../shared/constants'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import './editor.scss'; +import './style.scss'; + +/** + * Edit component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @param {string} props.clientId - Block client ID + * @return {JSX.Element} Counter Group edit component + */ +function CounterGroupEdit({ attributes, setAttributes, clientId }) { + const { + columns, + columnsTablet, + columnsMobile, + gap, + alignContent, + animationDuration, + animationDelay, + animationEasing, + useGrouping, + separator, + decimal, + hoverColor, + } = attributes; + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Block wrapper props + const blockProps = useBlockProps({ + className: 'airo-wp-counter-group', + style: { + // CRITICAL: Use align-self: stretch to fill parent width + alignSelf: 'stretch', + // Cast to string to prevent React from adding "px" suffix + '--airo-wp-counter-columns-desktop': String(columns), + '--airo-wp-counter-columns-tablet': String(columnsTablet), + '--airo-wp-counter-columns-mobile': String(columnsMobile), + '--airo-wp-counter-gap': gap, + // Apply hover color for child Counter blocks to inherit + ...(hoverColor && { + '--airo-wp-counter-hover-color': convertColorToCSSVar(hoverColor), + }), + }, + }); + + // Inner blocks configuration + const innerBlocksProps = useInnerBlocksProps( + { + className: `airo-wp-counter-group__inner airo-wp-counter-group__inner--align-${alignContent}`, + }, + { + allowedBlocks: ['airo-wp/counter'], + template: [ + [ + 'airo-wp/counter', + { + endValue: 500, + suffix: '+', + label: __('Happy Customers', 'airo-wp'), + }, + ], + [ + 'airo-wp/counter', + { + prefix: '$', + endValue: 1000, + suffix: 'K+', + label: __('Revenue Generated', 'airo-wp'), + }, + ], + [ + 'airo-wp/counter', + { + endValue: 99.9, + decimals: 1, + suffix: '%', + label: __('Uptime', 'airo-wp'), + }, + ], + ], + orientation: 'horizontal', + } + ); + + return ( + <> + + + setAttributes({ + hoverColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + + + + setAttributes({ + columns: 3, + columnsTablet: 2, + columnsMobile: 1, + gap: '32px', + alignContent: 'center', + animationDuration: 2, + animationDelay: 0, + animationEasing: 'easeOutQuad', + useGrouping: true, + separator: ',', + decimal: '.', + }) + } + > + columns !== 3} + onDeselect={() => setAttributes({ columns: 3 })} + isShownByDefault + > + + setAttributes({ columns: value }) + } + min={1} + max={6} + help={__('>1024px', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + columnsTablet !== 2} + onDeselect={() => setAttributes({ columnsTablet: 2 })} + isShownByDefault + > + + setAttributes({ columnsTablet: value }) + } + min={1} + max={columns} + help={__('768px - 1023px', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + columnsMobile !== 1} + onDeselect={() => setAttributes({ columnsMobile: 1 })} + isShownByDefault + > + + setAttributes({ columnsMobile: value }) + } + min={1} + max={columnsTablet} + help={__('<768px', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + gap !== '32px'} + onDeselect={() => setAttributes({ gap: '32px' })} + isShownByDefault + > + setAttributes({ gap: value })} + units={[ + { value: 'px', label: 'px' }, + { value: 'rem', label: 'rem' }, + { value: 'em', label: 'em' }, + ]} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + alignContent !== 'center'} + onDeselect={() => + setAttributes({ alignContent: 'center' }) + } + isShownByDefault + > + + setAttributes({ alignContent: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + animationDuration !== 2} + onDeselect={() => + setAttributes({ animationDuration: 2 }) + } + isShownByDefault + > + + setAttributes({ animationDuration: value }) + } + min={0.5} + max={5} + step={0.1} + help={__( + 'How long the count animation takes', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + animationDelay !== 0} + onDeselect={() => setAttributes({ animationDelay: 0 })} + isShownByDefault + > + + setAttributes({ animationDelay: value }) + } + min={0} + max={2} + step={0.1} + help={__( + 'Delay before animation starts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + animationEasing !== 'easeOutQuad'} + onDeselect={() => + setAttributes({ animationEasing: 'easeOutQuad' }) + } + isShownByDefault + > + + setAttributes({ animationEasing: value }) + } + help={__('Animation timing curve', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + useGrouping !== true} + onDeselect={() => setAttributes({ useGrouping: true })} + isShownByDefault + > + + setAttributes({ useGrouping: value }) + } + help={__( + 'Format numbers like "1,000" or "1000"', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + {useGrouping && ( + separator !== ','} + onDeselect={() => setAttributes({ separator: ',' })} + isShownByDefault + > + + setAttributes({ separator: value }) + } + help={__( + 'Character for thousands (e.g., "," or ".")', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + decimal !== '.'} + onDeselect={() => setAttributes({ decimal: '.' })} + isShownByDefault + > + + setAttributes({ decimal: value }) + } + help={__( + 'Character for decimals (e.g., "." or ",")', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    +
    +
    + + ); +} + +/** + * Register block + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + + + + + + ), + foreground: ICON_COLOR, + }, + edit: CounterGroupEdit, + save, +}); diff --git a/src/blocks/counter-group/save.js b/src/blocks/counter-group/save.js new file mode 100644 index 0000000..513df27 --- /dev/null +++ b/src/blocks/counter-group/save.js @@ -0,0 +1,64 @@ +/** + * Counter Group Block - Save Function + * + * WordPress Best Practice Approach: + * - Uses useInnerBlocksProps.save() for proper inner blocks integration + * - Declarative style application (matches edit.js exactly) + * - CSS custom properties for responsive grid + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +export default function CounterGroupSave({ attributes }) { + const { + columns, + columnsTablet, + columnsMobile, + gap, + alignContent, + animationDuration, + animationDelay, + animationEasing, + useGrouping, + separator, + decimal, + hoverColor, + } = attributes; + + // Block wrapper props with CSS custom properties + const blockProps = useBlockProps.save({ + className: 'airo-wp-counter-group', + style: { + // CRITICAL: Use align-self: stretch to fill parent width (must match index.js) + alignSelf: 'stretch', + // Cast to string to prevent React from adding "px" suffix + '--airo-wp-counter-columns-desktop': String(columns), + '--airo-wp-counter-columns-tablet': String(columnsTablet), + '--airo-wp-counter-columns-mobile': String(columnsMobile), + '--airo-wp-counter-gap': gap, + // Apply hover color for child Counter blocks to inherit + ...(hoverColor && { + '--airo-wp-counter-hover-color': convertColorToCSSVar(hoverColor), + }), + }, + // Data attributes for frontend JavaScript + 'data-animation-duration': animationDuration, + 'data-animation-delay': animationDelay, + 'data-animation-easing': animationEasing, + 'data-use-grouping': useGrouping ? 'true' : 'false', + 'data-separator': separator, + 'data-decimal': decimal, + }); + + // Inner blocks props (WordPress best practice) + const innerBlocksProps = useInnerBlocksProps.save({ + className: `airo-wp-counter-group__inner airo-wp-counter-group__inner--align-${alignContent}`, + }); + + return ( +
    +
    +
    + ); +} diff --git a/src/blocks/counter-group/style.scss b/src/blocks/counter-group/style.scss new file mode 100644 index 0000000..3720142 --- /dev/null +++ b/src/blocks/counter-group/style.scss @@ -0,0 +1,73 @@ +/** + * Counter Group Block - Frontend Styles + * + * Responsive grid layout using CSS custom properties + */ + +.airo-wp-counter-group { + box-sizing: border-box; + max-width: 100%; // Prevent overflow in containers and on small screens + + // Wide alignment - use negative margins to extend to wide width + &.alignwide { + width: var(--wp--style--global--wide-size); + max-width: 100%; + } + + // Full alignment - use negative margins to extend edge-to-edge + &.alignfull { + width: 100%; + max-width: 100%; + margin-right: calc(var(--wp--style--root--padding-right) * -1); + margin-left: calc(var(--wp--style--root--padding-left) * -1); + } + + // Default text color that child Counter blocks will inherit + // WordPress applies inline style when user chooses a color, which takes precedence + color: var(--wp--preset--color--contrast); + + &__inner { + display: grid; + grid-template-columns: repeat(var(--airo-wp-counter-columns-desktop, 3), 1fr); + gap: var(--airo-wp-counter-gap, 32px); + box-sizing: border-box; + + // WordPress may wrap inner blocks - make wrapper transparent so grid applies to counter blocks + > .wp-block-post-content, + > div:not([class]) { + display: contents; + } + + // Alignment classes (match class names from save.js/index.js) + &--align-left { + justify-items: start; + text-align: left; + } + + &--align-center { + justify-items: center; + text-align: center; + } + + &--align-right { + justify-items: end; + text-align: right; + } + } + + // Tablet breakpoint (768px - 1023px) + @media (max-width: 1023px) { + + &__inner { + grid-template-columns: repeat(var(--airo-wp-counter-columns-tablet, 2), 1fr); + } + } + + // Mobile breakpoint (<768px) + @media (max-width: 767px) { + + &__inner { + grid-template-columns: repeat(var(--airo-wp-counter-columns-mobile, 1), 1fr); + } + } +} diff --git a/src/blocks/counter-group/view.js b/src/blocks/counter-group/view.js new file mode 100644 index 0000000..29b54a6 --- /dev/null +++ b/src/blocks/counter-group/view.js @@ -0,0 +1,226 @@ +/** + * Counter Group Block - Frontend JavaScript + * + * Animated counting using CountUp.js + * Features: + * - Lazy-loaded animation with Intersection Observer + * - Respects prefers-reduced-motion accessibility setting + * - Easing functions for smooth animations + */ + +/* global IntersectionObserver */ + +import { CountUp } from 'countup.js'; + +// Track initialized groups to prevent duplicate CountUp instances on bfcache/soft nav +const initializedGroups = new WeakSet(); + +/** + * Initialize counter animations on page load + */ +document.addEventListener('DOMContentLoaded', () => { + initCounterAnimations(); +}); +document.addEventListener('airo-wp-content-loaded', initCounterAnimations); + +/** + * Initialize counter animations with lazy loading + */ +function initCounterAnimations() { + const counterGroups = document.querySelectorAll('.airo-wp-counter-group'); + + if (!counterGroups.length) { + return; + } + + // Check if user prefers reduced motion + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; + + // If user prefers reduced motion, show final values immediately + if (prefersReducedMotion) { + counterGroups.forEach((group) => { + const counters = group.querySelectorAll('.airo-wp-counter'); + counters.forEach((counter) => { + showFinalValue(counter); + }); + }); + return; + } + + // Use Intersection Observer for lazy loading + const counterObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + animateCountersInGroup(entry.target); + counterObserver.unobserve(entry.target); + } + }); + }, + { + rootMargin: '0px', // Start animation when element enters viewport + threshold: 0.1, // 10% visible + } + ); + + counterGroups.forEach((group) => { + // Prevent duplicate initialization (avoids CountUp leaks on bfcache) + if (initializedGroups.has(group)) { + return; + } + initializedGroups.add(group); + counterObserver.observe(group); + }); +} + +/** + * Animate all counters in a group + * @param {HTMLElement} group - Counter group element + */ +function animateCountersInGroup(group) { + const counters = group.querySelectorAll('.airo-wp-counter'); + + counters.forEach((counter) => { + animateCounter(counter); + }); +} + +/** + * Animate a single counter + * @param {HTMLElement} counter - Counter element + */ +function animateCounter(counter) { + const valueElement = counter.querySelector('.airo-wp-counter__value'); + if (!valueElement) { + return; + } + + // Get counter settings from data attributes + const startValue = parseFloat(counter.dataset.startValue) || 0; + const endValue = parseFloat(counter.dataset.endValue) || 100; + const decimals = parseInt(counter.dataset.decimals, 10) || 0; + const prefix = counter.dataset.prefix || ''; + const suffix = counter.dataset.suffix || ''; + const duration = parseFloat(counter.dataset.duration) || 2; + const delay = parseFloat(counter.dataset.delay) || 0; + const easing = counter.dataset.easing || 'easeOutQuad'; + const useGrouping = counter.dataset.useGrouping === 'true'; + const separator = counter.dataset.separator || ','; + const decimal = counter.dataset.decimal || '.'; + + // CountUp.js options + const options = { + startVal: startValue, + decimalPlaces: decimals, + duration, + useEasing: true, + useGrouping, + separator, + decimal, + prefix, + suffix, + enableScrollSpy: false, // We're using our own Intersection Observer + scrollSpyOnce: true, + }; + + // Map our easing names to CountUp.js easing functions + const easingFn = getEasingFunction(easing); + if (easingFn) { + options.easingFn = easingFn; + } + + // Create and start counter animation + const countUp = new CountUp(valueElement, endValue, options); + + if (!countUp.error) { + // Start after delay + setTimeout(() => { + countUp.start(); + }, delay * 1000); + } else { + // CountUp initialization failed, show final value as fallback + showFinalValue(counter); + } +} + +/** + * Escape special characters in replacement strings for use with String.replace() + * Prevents injection via special sequences like $&, $`, $', $n + * + * @param {string} str - String to escape + * @return {string} Escaped string safe for use as replacement + */ +function escapeReplacement(str) { + // Escape $ characters by doubling them ($ becomes $$) + // This prevents special replacement patterns like $&, $`, $', $1, etc. + return String(str).replace(/\$/g, '$$$$'); +} + +/** + * Show final value immediately (no animation) + * @param {HTMLElement} counter - Counter element + */ +function showFinalValue(counter) { + const valueElement = counter.querySelector('.airo-wp-counter__value'); + if (!valueElement) { + return; + } + + const endValue = parseFloat(counter.dataset.endValue) || 100; + const decimals = parseInt(counter.dataset.decimals, 10) || 0; + const useGrouping = counter.dataset.useGrouping === 'true'; + const separator = counter.dataset.separator || ','; + const decimal = counter.dataset.decimal || '.'; + + // Format number + let formatted = endValue.toFixed(decimals); + const parts = formatted.split('.'); + + // Add thousands separator + if (useGrouping) { + // SECURITY: Escape special characters in separator to prevent injection + // via replacement string special sequences ($&, $`, $', $n, etc.) + const safeSeparator = escapeReplacement(separator); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, safeSeparator); + } + + // Join with decimal point + formatted = parts.join(decimal); + + // Update value + valueElement.textContent = formatted; +} + +/** + * Get easing function by name + * @param {string} name - Easing function name + * @return {Function|null} Easing function + */ +function getEasingFunction(name) { + const easingFunctions = { + linear: (t, b, c, d) => { + return (c * t) / d + b; + }, + easeOutQuad: (t, b, c, d) => { + t /= d; + return -c * t * (t - 2) + b; + }, + easeOutCubic: (t, b, c, d) => { + t /= d; + t--; + return c * (t * t * t + 1) + b; + }, + easeInOutQuad: (t, b, c, d) => { + t /= d / 2; + if (t < 1) { + return (c / 2) * t * t + b; + } + t--; + return (-c / 2) * (t * (t - 2) - 1) + b; + }, + }; + + return easingFunctions[name] || null; +} diff --git a/src/blocks/counter/block.json b/src/blocks/counter/block.json new file mode 100644 index 0000000..5f3be7a --- /dev/null +++ b/src/blocks/counter/block.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/counter", + "version": "1.0.0", + "title": "Counter", + "category": "airo-wp", + "description": "Individual counter item with animated number", + "keywords": [ + "counter", + "stat", + "number" + ], + "textdomain": "airo-wp", + "icon": "chart-bar", + "parent": [ + "airo-wp/counter-group" + ], + "supports": { + "anchor": false, + "html": false, + "inserter": false, + "reusable": false, + "spacing": { + "margin": false, + "padding": { + "top": true, + "right": true, + "bottom": true, + "left": true + } + }, + "color": { + "background": true, + "text": true, + "__experimentalDefaultControls": { + "background": false, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true + } + } + }, + "attributes": { + "uniqueId": { + "type": "string", + "default": "" + }, + "startValue": { + "type": "number", + "default": 0 + }, + "endValue": { + "type": "number", + "default": 100 + }, + "decimals": { + "type": "number", + "default": 0 + }, + "prefix": { + "type": "string", + "default": "" + }, + "suffix": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "" + }, + "showIcon": { + "type": "boolean", + "default": false + }, + "icon": { + "type": "string", + "default": "star" + }, + "iconPosition": { + "type": "string", + "default": "top" + }, + "overrideAnimation": { + "type": "boolean", + "default": false + }, + "customDuration": { + "type": "number", + "default": 2 + }, + "customDelay": { + "type": "number", + "default": 0 + }, + "customEasing": { + "type": "string", + "default": "easeOutQuad" + }, + "hoverColor": { + "type": "string", + "default": "" + } + }, + "usesContext": [ + "airo-wp/counterGroup/animationDuration", + "airo-wp/counterGroup/animationDelay", + "airo-wp/counterGroup/animationEasing", + "airo-wp/counterGroup/useGrouping", + "airo-wp/counterGroup/separator", + "airo-wp/counterGroup/decimal", + "airo-wp/counterGroup/hoverColor" + ], + "example": { + "attributes": { + "endValue": 1250, + "suffix": "+", + "label": "Happy Clients", + "showIcon": true, + "icon": "star", + "iconPosition": "top" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/counter/components/inspector/AnimationPanel.js b/src/blocks/counter/components/inspector/AnimationPanel.js new file mode 100644 index 0000000..906171f --- /dev/null +++ b/src/blocks/counter/components/inspector/AnimationPanel.js @@ -0,0 +1,180 @@ +/** + * Counter Block - Animation Panel Component + * + * Renders DsgoInspectorPanel.Item entries for animation override. + * Meant to be composed inside the Settings DsgoInspectorPanel in + * counter/edit.js. + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { + ToggleControl, + RangeControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +export const AnimationPanel = ({ + overrideAnimation, + customDuration, + customDelay, + customEasing, + context, + setAttributes, +}) => { + // Get parent settings from context (with fallback defaults) + const parentDuration = + context?.['airo-wp/counterGroup/animationDuration'] || 2; + const parentDelay = + context?.['airo-wp/counterGroup/animationDelay'] || 0; + const parentEasing = + context?.['airo-wp/counterGroup/animationEasing'] || 'easeOutQuad'; + + return ( + <> + overrideAnimation !== false} + onDeselect={() => setAttributes({ overrideAnimation: false })} + isShownByDefault + > + + setAttributes({ overrideAnimation: value }) + } + help={__( + 'Use custom animation settings instead of parent settings', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + {!overrideAnimation && ( +
    +

    + + {__('Using parent settings:', 'airo-wp')} + +
    + {__('Duration:', 'airo-wp')} {parentDuration}s +
    + {__('Delay:', 'airo-wp')} {parentDelay}s +
    + {__('Easing:', 'airo-wp')} {parentEasing} +

    +
    + )} +
    + + {overrideAnimation && ( + customDuration !== 2} + onDeselect={() => setAttributes({ customDuration: 2 })} + isShownByDefault + > + + setAttributes({ customDuration: value }) + } + min={0.5} + max={5} + step={0.1} + help={__( + 'How long the counting animation takes', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {overrideAnimation && ( + customDelay !== 0} + onDeselect={() => setAttributes({ customDelay: 0 })} + isShownByDefault + > + + setAttributes({ customDelay: value }) + } + min={0} + max={2} + step={0.1} + help={__( + 'Delay before animation starts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {overrideAnimation && ( + customEasing !== 'easeOutQuad'} + onDeselect={() => + setAttributes({ customEasing: 'easeOutQuad' }) + } + isShownByDefault + > + + setAttributes({ customEasing: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + ); +}; diff --git a/src/blocks/counter/components/inspector/CounterSettingsPanel.js b/src/blocks/counter/components/inspector/CounterSettingsPanel.js new file mode 100644 index 0000000..fd10241 --- /dev/null +++ b/src/blocks/counter/components/inspector/CounterSettingsPanel.js @@ -0,0 +1,121 @@ +/** + * Counter Block - Counter Settings Panel Component + * + * Renders DsgoInspectorPanel.Item entries for counter values, decimals, + * and prefix/suffix. Meant to be composed inside the Settings + * DsgoInspectorPanel in counter/edit.js. + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { RangeControl, TextControl } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +export const CounterSettingsPanel = ({ + startValue, + endValue, + decimals, + prefix, + suffix, + setAttributes, +}) => { + return ( + <> + startValue !== 0} + onDeselect={() => setAttributes({ startValue: 0 })} + isShownByDefault + > + setAttributes({ startValue: value })} + min={0} + max={endValue} + help={__('Number to count from', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + endValue !== 100} + onDeselect={() => setAttributes({ endValue: 100 })} + isShownByDefault + > + + setAttributes({ endValue: parseFloat(value) || 0 }) + } + help={__('Final number to display', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + decimals !== 0} + onDeselect={() => setAttributes({ decimals: 0 })} + isShownByDefault + > + setAttributes({ decimals: value })} + min={0} + max={3} + help={__('Number of decimal places', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + prefix !== ''} + onDeselect={() => setAttributes({ prefix: '' })} + isShownByDefault + > + setAttributes({ prefix: value })} + placeholder="$" + help={__( + 'Text before number (e.g., "$", "€")', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + suffix !== ''} + onDeselect={() => setAttributes({ suffix: '' })} + isShownByDefault + > + setAttributes({ suffix: value })} + placeholder="+" + help={__( + 'Text after number (e.g., "+", "%", "K")', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + ); +}; diff --git a/src/blocks/counter/components/inspector/IconSettingsPanel.js b/src/blocks/counter/components/inspector/IconSettingsPanel.js new file mode 100644 index 0000000..63a3cc8 --- /dev/null +++ b/src/blocks/counter/components/inspector/IconSettingsPanel.js @@ -0,0 +1,118 @@ +/** + * Counter Block - Icon Settings Panel Component + * + * Renders DsgoInspectorPanel.Item entries for icon show/hide, type, and + * position. Meant to be composed inside the Settings DsgoInspectorPanel + * in counter/edit.js. + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { ToggleControl, SelectControl } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +export const IconSettingsPanel = ({ + showIcon, + icon, + iconPosition, + setAttributes, +}) => { + return ( + <> + showIcon !== false} + onDeselect={() => setAttributes({ showIcon: false })} + isShownByDefault + > + setAttributes({ showIcon: value })} + help={ + showIcon + ? __('Icon is displayed', 'airo-wp') + : __('No icon displayed', 'airo-wp') + } + __nextHasNoMarginBottom + /> + + + {showIcon && ( + icon !== 'star'} + onDeselect={() => setAttributes({ icon: 'star' })} + isShownByDefault + > + setAttributes({ icon: value })} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {showIcon && ( + iconPosition !== 'top'} + onDeselect={() => setAttributes({ iconPosition: 'top' })} + isShownByDefault + > + + setAttributes({ iconPosition: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + ); +}; diff --git a/src/blocks/counter/components/inspector/LabelSettingsPanel.js b/src/blocks/counter/components/inspector/LabelSettingsPanel.js new file mode 100644 index 0000000..129b137 --- /dev/null +++ b/src/blocks/counter/components/inspector/LabelSettingsPanel.js @@ -0,0 +1,33 @@ +/** + * Counter Block - Label Settings Panel Component + * + * Renders DsgoInspectorPanel.Item for the counter label. Meant to be + * composed inside the Settings DsgoInspectorPanel in counter/edit.js. + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { TextControl } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../../../components/shared'; + +export const LabelSettingsPanel = ({ label, setAttributes }) => { + return ( + label !== ''} + onDeselect={() => setAttributes({ label: '' })} + isShownByDefault + > + setAttributes({ label: value })} + placeholder={__('Enter label…', 'airo-wp')} + help={__('Description text below counter', 'airo-wp')} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + ); +}; diff --git a/src/blocks/counter/edit.js b/src/blocks/counter/edit.js new file mode 100644 index 0000000..5a097cd --- /dev/null +++ b/src/blocks/counter/edit.js @@ -0,0 +1,253 @@ +/** + * Counter Block - Edit Component (Refactored for Maintainability) + * + * Individual counter item that displays an animated counting number. + * Gets animation and formatting settings from parent Counter Group. + * + * File size: ~120 lines (down from 357 lines in index.js - 66% reduction!) + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + InspectorControls, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, + // WordPress 6.5+ - useSettings (plural) replaces useSetting (singular) + useSettings, +} from '@wordpress/block-editor'; +import { useEffect } from '@wordpress/element'; + +// Extracted Inspector Panel Components +import { DsgoInspectorPanel } from '../../components/shared'; +import { CounterSettingsPanel } from './components/inspector/CounterSettingsPanel'; +import { LabelSettingsPanel } from './components/inspector/LabelSettingsPanel'; +import { IconSettingsPanel } from './components/inspector/IconSettingsPanel'; +import { AnimationPanel } from './components/inspector/AnimationPanel'; + +// Extracted Utilities +import { formatCounterValue } from './utils/number-formatter'; +import { getIconSvg } from './utils/icon-library'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +/** + * Counter Edit Component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @param {Object} props.context - Block context from parent Counter Group + * @param {string} props.clientId - Block client ID + * @return {JSX.Element} Counter block edit component + */ +export default function CounterEdit({ + attributes, + setAttributes, + context, + clientId, +}) { + const { + uniqueId, + startValue, + endValue, + decimals, + prefix, + suffix, + label, + showIcon, + icon, + iconPosition, + overrideAnimation, + customDuration, + customDelay, + customEasing, + hoverColor, + } = attributes; + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Get theme color palette (WordPress 6.5+ - useSettings returns array) + const [colorSettings] = useSettings('color.palette'); + + // Get formatting settings from parent Counter Group context (with fallback defaults) + const parentUseGrouping = + context?.['airo-wp/counterGroup/useGrouping'] ?? true; + const parentSeparator = + context?.['airo-wp/counterGroup/separator'] || ','; + const parentDecimal = context?.['airo-wp/counterGroup/decimal'] || '.'; + const parentHoverColor = + context?.['airo-wp/counterGroup/hoverColor'] || ''; + + // Get theme accent-2 color as default + const themeColors = colorSettings?.theme || []; + const accent2Color = themeColors.find((color) => color.slug === 'accent-2'); + const defaultHoverColor = accent2Color?.color || ''; + + // Determine effective hover color: individual override > parent > theme accent-2 + const effectiveHoverColor = + hoverColor || parentHoverColor || defaultHoverColor; + + // Generate unique ID on mount (acceptable use of useEffect for ID generation) + useEffect(() => { + if (!uniqueId) { + setAttributes({ + uniqueId: `counter-${Math.random().toString(36).substr(2, 9)}`, + }); + } + }, [uniqueId, setAttributes]); + + // Calculate display value using extracted utility + const displayValue = formatCounterValue(endValue, { + prefix, + suffix, + decimals, + useGrouping: parentUseGrouping, + separator: parentSeparator, + decimal: parentDecimal, + }); + + // Block wrapper props + const blockProps = useBlockProps({ + className: 'airo-wp-counter', + style: { + textAlign: 'center', + // Apply effective hover color as CSS custom property + ...(effectiveHoverColor && { + '--airo-wp-counter-hover-color': + convertColorToCSSVar(effectiveHoverColor), + }), + }, + }); + + return ( + <> + {/* ======================================== + INSPECTOR CONTROLS + ======================================== */} + + + setAttributes({ + hoverColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + + + + setAttributes({ + startValue: 0, + endValue: 100, + decimals: 0, + prefix: '', + suffix: '', + label: '', + showIcon: false, + icon: 'star', + iconPosition: 'top', + overrideAnimation: false, + customDuration: 2, + customDelay: 0, + customEasing: 'easeOutQuad', + }) + } + > + + + + + + + + + + + {/* ======================================== + BLOCK CONTENT + ======================================== */} +
    + {/* Icon (if enabled and position is top) */} + {showIcon && iconPosition === 'top' && ( +
    + {getIconSvg(icon)} +
    + )} + +
    + {/* Icon (if enabled and position is left) */} + {showIcon && iconPosition === 'left' && ( +
    + {getIconSvg(icon)} +
    + )} + + {/* Number */} +
    {displayValue}
    + + {/* Icon (if enabled and position is right) */} + {showIcon && iconPosition === 'right' && ( +
    + {getIconSvg(icon)} +
    + )} +
    + + {/* Label */} + {label &&
    {label}
    } +
    + + ); +} diff --git a/src/blocks/counter/editor.scss b/src/blocks/counter/editor.scss new file mode 100644 index 0000000..57aafc2 --- /dev/null +++ b/src/blocks/counter/editor.scss @@ -0,0 +1,52 @@ +/** + * Counter Block - Editor Styles + * + * Editor-specific styles (matches frontend for WYSIWYG) + */ + +.airo-wp-counter { + // CRITICAL: Must be duplicated from style.scss for editor/frontend parity + // Inherit color from parent Counter Group by default + // WordPress applies inline style when user chooses a color on individual counter, which takes precedence + // Falls back to theme contrast color if no parent color is set + color: inherit; + + // Editor-specific styles to improve UX + position: relative; + + // Selected state + &.is-selected { + outline: 1px solid var(--wp-admin-theme-color, #007cba); + outline-offset: 2px; + border-radius: 4px; + } + + // Hover state in editor + &.is-hovered { + outline: 1px dashed var(--wp-admin-theme-color, #007cba); + outline-offset: 2px; + border-radius: 4px; + } + + // Editor-only: Show counter value in a more editable format + .block-editor-rich-text__editable { + display: inline; + } + + // CRITICAL: Must be duplicated from style.scss for editor/frontend parity + // Number color - inherits from wrapper + &__number { + color: currentcolor; + } + + // Hover state - must match frontend + &:hover { + + .airo-wp-counter__number { + color: var( + --airo-wp-counter-hover-color, + var(--wp--preset--color--accent-2, currentColor) + ); + } + } +} diff --git a/src/blocks/counter/index.js b/src/blocks/counter/index.js new file mode 100644 index 0000000..0aaed74 --- /dev/null +++ b/src/blocks/counter/index.js @@ -0,0 +1,48 @@ +/** + * Counter Block - Registration + * + * Registers the Counter block with WordPress. + * Edit component extracted to edit.js for better maintainability. + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +/** + * Register Counter Block + * + * Individual counter item within a Counter Group block. + * Displays an animated counting number with optional icon and label. + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, +}); diff --git a/src/blocks/counter/save.js b/src/blocks/counter/save.js new file mode 100644 index 0000000..6b02edd --- /dev/null +++ b/src/blocks/counter/save.js @@ -0,0 +1,214 @@ +/** + * Counter Block - Save Function + * + * WordPress Best Practice Approach: + * - Declarative style application (matches edit.js exactly) + * - Data attributes for frontend JavaScript animation + */ + +import { useBlockProps } from '@wordpress/block-editor'; + +export default function CounterSave({ attributes, context }) { + const { + uniqueId, + startValue, + endValue, + decimals, + prefix, + suffix, + label, + showIcon, + icon, + iconPosition, + overrideAnimation, + customDuration, + customDelay, + customEasing, + hoverColor, + } = attributes; + + // Get settings from parent Counter Group context (with fallback defaults) + const parentDuration = + context?.['airo-wp/counterGroup/animationDuration'] || 2; + const parentDelay = + context?.['airo-wp/counterGroup/animationDelay'] || 0; + const parentEasing = + context?.['airo-wp/counterGroup/animationEasing'] || 'easeOutQuad'; + const parentUseGrouping = + context?.['airo-wp/counterGroup/useGrouping'] ?? true; + const parentSeparator = + context?.['airo-wp/counterGroup/separator'] || ','; + const parentDecimal = context?.['airo-wp/counterGroup/decimal'] || '.'; + const parentHoverColor = + context?.['airo-wp/counterGroup/hoverColor'] || ''; + + // Determine active animation settings + const activeDuration = overrideAnimation ? customDuration : parentDuration; + const activeDelay = overrideAnimation ? customDelay : parentDelay; + const activeEasing = overrideAnimation ? customEasing : parentEasing; + + // Determine effective hover color: individual override > parent + const effectiveHoverColor = hoverColor || parentHoverColor; + + // Block wrapper props + const blockProps = useBlockProps.save({ + className: 'airo-wp-counter', + id: uniqueId, + style: { + textAlign: 'center', + // Apply effective hover color as CSS custom property + ...(effectiveHoverColor && { + '--airo-wp-counter-hover-color': effectiveHoverColor, + }), + }, + // Data attributes for frontend JavaScript + 'data-start-value': startValue, + 'data-end-value': endValue, + 'data-decimals': decimals, + 'data-prefix': prefix || '', + 'data-suffix': suffix || '', + 'data-duration': activeDuration, + 'data-delay': activeDelay, + 'data-easing': activeEasing, + 'data-use-grouping': parentUseGrouping ? 'true' : 'false', + 'data-separator': parentSeparator || ',', + 'data-decimal': parentDecimal || '.', + }); + + return ( +
    + {/* Icon (if enabled and position is top) */} + {showIcon && iconPosition === 'top' && ( +
    + {getIconSvg(icon)} +
    + )} + +
    + {/* Icon (if enabled and position is left) */} + {showIcon && iconPosition === 'left' && ( +
    + {getIconSvg(icon)} +
    + )} + + {/* Number - Will be animated by frontend JavaScript */} +
    + {/* Initial value (0 or startValue), will be animated to endValue */} + {startValue} +
    + + {/* Icon (if enabled and position is right) */} + {showIcon && iconPosition === 'right' && ( +
    + {getIconSvg(icon)} +
    + )} +
    + + {/* Label */} + {label &&
    {label}
    } +
    + ); +} + +/** + * Get SVG for icon (same as edit.js for editor/frontend parity) + * + * @param {string} iconName - Name of the icon to render + * @return {JSX.Element} SVG icon element + */ +function getIconSvg(iconName) { + const icons = { + star: ( + + + + ), + trophy: ( + + + + ), + heart: ( + + + + ), + check: ( + + + + ), + dollar: ( + + + + ), + users: ( + + + + ), + chart: ( + + + + ), + rocket: ( + + + + ), + }; + + return icons[iconName] || icons.star; +} diff --git a/src/blocks/counter/style.scss b/src/blocks/counter/style.scss new file mode 100644 index 0000000..2b34dc5 --- /dev/null +++ b/src/blocks/counter/style.scss @@ -0,0 +1,134 @@ +/** + * Counter Block - Frontend Styles + * + * Individual counter item with icon, number, and label + */ + +.airo-wp-counter { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1.5rem; + box-sizing: border-box; + // Inherit color from parent Counter Group by default + // WordPress applies inline style when user chooses a color on individual counter, which takes precedence + // Falls back to theme contrast color if no parent color is set + color: inherit; + + // Icon + &__icon { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; + // Inherit color from parent or use currentColor as fallback + color: currentcolor; + opacity: 0.9; + + svg { + width: 48px; + max-width: 100%; + height: auto; + } + + &--top { + margin-bottom: 1rem; + } + + &--left { + margin-right: 1rem; + margin-bottom: 0; + } + + &--right { + margin-left: 1rem; + margin-bottom: 0; + } + } + + // Content wrapper (number + optional side icons) + &__content { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + + &--icon-left, + &--icon-right { + flex-direction: row; + } + } + + // Number + &__number { + // Use em units to scale with parent font-size (WordPress typography controls) + font-size: 3em; + font-weight: 700; + line-height: 1.2; + // Inherit from .airo-wp-counter wrapper, which has theme contrast color as default + // If user sets color via WordPress panel, inline style on wrapper takes precedence + // This prevents inheriting from grandparent containers (like Container blocks) + color: currentcolor; + margin: 0; + transition: color 0.3s ease; + + .airo-wp-counter__value { + font-variant-numeric: tabular-nums; + } + } + + // Label + &__label { + // Use em units to scale with parent font-size + font-size: 1em; + // Use opacity for visual hierarchy while respecting parent color + opacity: 0.8; + margin-top: 0.5rem; + margin-bottom: 0; + line-height: 1.5; + transition: color 0.3s ease, opacity 0.3s ease; + } + + // Hover state + &:hover { + + .airo-wp-counter__number { + // Priority: custom hover color > theme accent-2 > counter's text color (currentColor) + // If user sets hover color via panel, it's applied as CSS custom property + // Otherwise falls back to theme accent color, then counter's own text color + color: var( + --airo-wp-counter-hover-color, + var(--wp--preset--color--accent-2, currentColor) + ); + } + + .airo-wp-counter__label { + opacity: 1; + } + } + + // Accessibility: Respect prefers-reduced-motion + @media (prefers-reduced-motion: reduce) { + + .airo-wp-counter__number, + .airo-wp-counter__label { + transition: none; + } + } + + // Responsive: Smaller number on mobile + @media (max-width: 767px) { + padding: 1rem; + + &__number { + // Scale down proportionally on mobile + font-size: 2.5em; + } + + &__icon svg { + width: 36px; + height: 36px; + } + } +} diff --git a/src/blocks/counter/utils/icon-library.js b/src/blocks/counter/utils/icon-library.js new file mode 100644 index 0000000..86c3b50 --- /dev/null +++ b/src/blocks/counter/utils/icon-library.js @@ -0,0 +1,117 @@ +/** + * Counter Block - Icon Library + * + * SVG icons for counter block. + * Separated for reusability and maintainability. + * + * @since 1.0.0 + */ + +/** + * Get SVG element for specified icon. + * + * @param {string} iconName - Icon name (star, trophy, heart, check, dollar, users, chart, rocket) + * @return {JSX.Element} SVG icon element + */ +export const getIconSvg = (iconName) => { + const icons = { + star: ( + + ), + trophy: ( + + ), + heart: ( + + ), + check: ( + + ), + dollar: ( + + ), + users: ( + + ), + chart: ( + + ), + rocket: ( + + ), + }; + + return icons[iconName] || icons.star; +}; diff --git a/src/blocks/counter/utils/number-formatter.js b/src/blocks/counter/utils/number-formatter.js new file mode 100644 index 0000000..3e6515e --- /dev/null +++ b/src/blocks/counter/utils/number-formatter.js @@ -0,0 +1,77 @@ +/** + * Counter Block - Number Formatter Utility + * + * Pure function for formatting counter numbers with grouping and decimals. + * Separated for testability and reusability. + * + * @since 1.0.0 + */ + +/** + * Escape special characters in replacement strings for use with String.replace() + * Prevents injection via special sequences like $&, $`, $', $n + * + * @param {string} str - String to escape + * @return {string} Escaped string safe for use as replacement + */ +const escapeReplacement = (str) => { + // Escape $ characters by doubling them ($ becomes $$) + // This prevents special replacement patterns like $&, $`, $', $1, etc. + return String(str).replace(/\$/g, '$$$$'); +}; + +/** + * Format a number with thousands separator and decimal places. + * + * @param {number} value - The number to format + * @param {Object} options - Formatting options + * @param {number} options.decimals - Number of decimal places (0-3) + * @param {boolean} options.useGrouping - Whether to use thousands separator + * @param {string} options.separator - Thousands separator (e.g., ',') + * @param {string} options.decimal - Decimal point character (e.g., '.') + * @return {string} Formatted number string + */ +export const formatNumber = (value, options = {}) => { + const { + decimals = 0, + useGrouping = true, + separator = ',', + decimal = '.', + } = options; + + // Format with decimals + const formatted = value.toFixed(decimals); + const parts = formatted.split('.'); + + // Add thousands separator if enabled + if (useGrouping) { + // SECURITY: Escape special characters in separator to prevent injection + // via replacement string special sequences ($&, $`, $', $n, etc.) + const safeSeparator = escapeReplacement(separator); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, safeSeparator); + } + + // Join with decimal point + return parts.join(decimal); +}; + +/** + * Format a counter value with prefix and suffix. + * + * @param {number} value - The number value + * @param {Object} options - Formatting options + * @param {string} options.prefix - Text before number (e.g., '$') + * @param {string} options.suffix - Text after number (e.g., '+', '%') + * @param {number} options.decimals - Number of decimal places + * @param {boolean} options.useGrouping - Whether to use thousands separator + * @param {string} options.separator - Thousands separator + * @param {string} options.decimal - Decimal point character + * @return {string} Fully formatted display value + */ +export const formatCounterValue = (value, options = {}) => { + const { prefix = '', suffix = '', ...formatOptions } = options; + + const formattedNumber = formatNumber(value, formatOptions); + + return `${prefix}${formattedNumber}${suffix}`; +}; diff --git a/src/blocks/divider/block.json b/src/blocks/divider/block.json new file mode 100644 index 0000000..0ffa8e5 --- /dev/null +++ b/src/blocks/divider/block.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/divider", + "version": "1.0.0", + "title": "Divider", + "category": "airo-wp", + "description": "Add a visual separator with multiple style options including solid, dashed, gradient, and decorative patterns.", + "keywords": [ + "divider", + "separator", + "line", + "hr", + "break" + ], + "textdomain": "airo-wp", + "icon": "minus", + "supports": { + "anchor": true, + "align": [ + "left", + "center", + "right", + "wide", + "full" + ], + "html": false, + "inserter": true, + "spacing": { + "margin": true, + "padding": false, + "__experimentalDefaultControls": { + "margin": true + } + }, + "color": { + "text": true, + "gradient": false, + "__experimentalDefaultControls": { + "text": true + } + }, + "dimensions": { + "minHeight": true, + "__experimentalDefaultControls": { + "minHeight": false + } + } + }, + "attributes": { + "dividerStyle": { + "type": "string", + "default": "solid", + "enum": [ + "solid", + "dashed", + "dotted", + "double", + "gradient", + "dots", + "wave", + "icon" + ] + }, + "width": { + "type": "number", + "default": 100 + }, + "thickness": { + "type": "number", + "default": 2 + }, + "iconName": { + "type": "string", + "default": "star" + }, + "iconStyle": { + "type": "string", + "enum": ["filled", "outlined"] + }, + "strokeWidth": { + "type": "number", + "default": 1.5 + } + }, + "example": { + "attributes": { + "dividerStyle": "gradient", + "width": 80 + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/divider/deprecated.js b/src/blocks/divider/deprecated.js new file mode 100644 index 0000000..ccbbfe0 --- /dev/null +++ b/src/blocks/divider/deprecated.js @@ -0,0 +1,200 @@ +/** + * Divider Block - Deprecated Versions + * + * Handles backward compatibility for blocks saved with previous versions. + * + * @since 1.2.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import { getIcon } from '../shared/icon-utils'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Shared supports for all deprecated versions. + */ +const sharedSupports = { + anchor: true, + align: ['left', 'center', 'right', 'wide', 'full'], + html: false, + inserter: true, + spacing: { + margin: true, + padding: false, + __experimentalDefaultControls: { + margin: true, + }, + }, + color: { + text: true, + gradient: false, + __experimentalDefaultControls: { + text: true, + }, + }, + dimensions: { + minHeight: true, + __experimentalDefaultControls: { + minHeight: false, + }, + }, +}; + +/** + * Lazy-placeholder format — the last STATIC version, before the Divider became + * server-rendered (dynamic). + * + * Static dividers using the "icon" style saved a `.airo-wp-lazy-icon` placeholder + * injected client-side; the dynamic block now renders the SVG in PHP + * (render.php) and its save() returns null. This deprecation reproduces the + * placeholder markup and migrates existing content untouched (passthrough), + * avoiding the "Attempt Recovery" warning. + */ +const vLazy = { + supports: sharedSupports, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + return Boolean(innerHTML) && innerHTML.includes('airo-wp-lazy-icon'); + }, + attributes: { + dividerStyle: { type: 'string', default: 'solid' }, + width: { type: 'number', default: 100 }, + thickness: { type: 'number', default: 2 }, + iconName: { type: 'string', default: 'star' }, + iconStyle: { type: 'string', enum: ['filled', 'outlined'] }, + strokeWidth: { type: 'number', default: 1.5 }, + }, + save({ attributes }) { + const { + dividerStyle, + width, + thickness, + iconName, + iconStyle, + strokeWidth, + } = attributes; + + const blockProps = useBlockProps.save({ + className: `airo-wp-divider airo-wp-divider--${dividerStyle}`, + }); + + const containerStyle = { width: `${width}%` }; + const lineStyle = { height: `${thickness}px` }; + + return ( +
    +
    + {dividerStyle === 'icon' ? ( +
    + + + +
    + ) : ( +
    + )} +
    +
    + ); + }, + migrate(attributes) { + return attributes; + }, +}; + +/** + * Version 1: Before lazy loading icon library + * + * Changes in current version: + * - Icons now use data attributes for frontend lazy loading + * - Frontend icons injected via PHP to avoid bundling 51KB library + */ +const v1 = { + supports: sharedSupports, + attributes: { + dividerStyle: { + type: 'string', + default: 'solid', + }, + width: { + type: 'number', + default: 100, + }, + thickness: { + type: 'number', + default: 2, + }, + iconName: { + type: 'string', + default: 'star', + }, + }, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v1 blocks have inline SVG icons instead of airo-wp-lazy-icon class + return innerHTML && !innerHTML.includes('airo-wp-lazy-icon'); + }, + save({ attributes }) { + const { dividerStyle, width, thickness, iconName } = attributes; + + // Block wrapper props + const blockProps = useBlockProps.save({ + className: `airo-wp-divider airo-wp-divider--${dividerStyle}`, + }); + + // Divider container styles + const containerStyle = { + width: `${width}%`, + }; + + // Divider line styles + const lineStyle = { + height: `${thickness}px`, + }; + + return ( +
    +
    + {dividerStyle === 'icon' ? ( +
    + + + {getIcon(iconName)} + + +
    + ) : ( +
    + )} +
    +
    + ); + }, + migrate(attributes) { + // No attribute changes needed - only save function changed + return attributes; + }, +}; + +export default [vLazy, v1]; diff --git a/src/blocks/divider/edit.js b/src/blocks/divider/edit.js new file mode 100644 index 0000000..7fcfdf3 --- /dev/null +++ b/src/blocks/divider/edit.js @@ -0,0 +1,291 @@ +/** + * Divider Block - Edit Component + * + * Visual separator with multiple style options including + * solid, dashed, gradient, and decorative patterns. + * + * @since 1.0.0 + */ + +import { __, sprintf } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + SelectControl, + RangeControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControl as ToggleGroupControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { getIcon, IconPicker } from '../shared/icon-utils'; +import { useIconDefaults } from '../../hooks'; + +/** + * Divider Edit Component + * + * @param {Object} props - Component props + * @param {Object} props.attributes - Block attributes + * @param {Function} props.setAttributes - Function to update attributes + * @param {string} props.clientId - Block client ID + * @return {JSX.Element} Divider block edit component + */ +export default function DividerEdit({ attributes, setAttributes, clientId }) { + const { dividerStyle, width, thickness, iconName, iconStyle, strokeWidth } = + attributes; + + // Theme-level icon defaults inherited when style is left unset. + const iconDefaults = useIconDefaults(); + const effectiveStyle = iconStyle || iconDefaults.style; + + // Block wrapper props - Block Supports automatically applies color styles + const blockProps = useBlockProps({ + className: `airo-wp-divider airo-wp-divider--${dividerStyle}`, + }); + + // Divider container styles + const containerStyle = { + width: `${width}%`, + }; + + // Divider line styles + const lineStyle = { + height: `${thickness}px`, + }; + + return ( + <> + {/* ======================================== + INSPECTOR CONTROLS - SETTINGS TAB + ======================================== */} + + + setAttributes({ + dividerStyle: 'solid', + width: 100, + thickness: 2, + iconName: 'star', + iconStyle: undefined, + strokeWidth: 1.5, + }) + } + > + dividerStyle !== 'solid'} + onDeselect={() => + setAttributes({ dividerStyle: 'solid' }) + } + isShownByDefault + > + + setAttributes({ dividerStyle: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {dividerStyle === 'icon' && ( + iconName !== 'star'} + onDeselect={() => + setAttributes({ iconName: 'star' }) + } + isShownByDefault + > + + setAttributes({ iconName: value }) + } + /> + + )} + + {dividerStyle === 'icon' && ( + typeof iconStyle === 'string'} + onDeselect={() => + setAttributes({ iconStyle: undefined }) + } + isShownByDefault + > + + setAttributes({ iconStyle: value }) + } + help={ + !iconStyle && + sprintf( + /* translators: %s: inherited icon style (Filled or Outlined). */ + __( + 'Inheriting theme default (%s).', + 'airo-wp' + ), + iconDefaults.style === 'outlined' + ? __('Outlined', 'airo-wp') + : __('Filled', 'airo-wp') + ) + } + isBlock + __nextHasNoMarginBottom + > + + + + + )} + + {dividerStyle === 'icon' && + effectiveStyle === 'outlined' && ( + strokeWidth !== 1.5} + onDeselect={() => + setAttributes({ strokeWidth: 1.5 }) + } + isShownByDefault + > + + setAttributes({ strokeWidth: value }) + } + min={0.5} + max={4} + step={0.5} + help={__( + 'Thinner strokes work better for detailed icons', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + width !== 100} + onDeselect={() => setAttributes({ width: 100 })} + isShownByDefault + > + + setAttributes({ width: value }) + } + min={10} + max={100} + step={5} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {dividerStyle !== 'icon' && ( + thickness !== 2} + onDeselect={() => setAttributes({ thickness: 2 })} + isShownByDefault + > + + setAttributes({ thickness: value }) + } + min={1} + max={20} + step={1} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + + + {/* ======================================== + BLOCK CONTENT + ======================================== */} +
    +
    + {dividerStyle === 'icon' ? ( +
    + + + {getIcon(iconName, effectiveStyle, strokeWidth)} + + +
    + ) : ( +
    + )} +
    +
    + + ); +} diff --git a/src/blocks/divider/editor.scss b/src/blocks/divider/editor.scss new file mode 100644 index 0000000..adc8114 --- /dev/null +++ b/src/blocks/divider/editor.scss @@ -0,0 +1,80 @@ +/** + * Divider Block - Editor Styles + * + * Editor-specific styles (matches frontend for WYSIWYG) + * + * @since 1.0.0 + */ + +.airo-wp-divider { + // CRITICAL: Must be duplicated from style.scss for editor/frontend parity + // (Main styles are in style.scss and automatically applied in editor) + + // CRITICAL: Ensure divider takes full width of parent container + // Without this, divider can collapse to 0 width inside flex containers + width: 100%; + + // Editor-specific styles to improve UX + position: relative; + min-height: 20px; // Ensure block is selectable even with thin dividers + display: flex; + align-items: center; + + // Selected state + &.is-selected { + outline: 1px solid var(--wp-admin-theme-color, #007cba); + outline-offset: 4px; + border-radius: 2px; + } + + // Hover state in editor + &:hover { + outline: 1px dashed var(--wp-admin-theme-color, #007cba); + outline-offset: 4px; + border-radius: 2px; + cursor: pointer; + } + + // Ensure the divider is visible in the editor + // NOTE: width is set via inline style from the width attribute (percentage) + &__container { + pointer-events: none; // Allow click-through to select the block + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; // Default center alignment + } + + // Left alignment + &.alignleft &__container { + margin-left: 0; + margin-right: auto; + justify-content: flex-start; + } + + // Right alignment + &.alignright &__container { + margin-left: auto; + margin-right: 0; + justify-content: flex-end; + } + + // Center alignment (explicit) + &.aligncenter &__container { + margin: 0 auto; + justify-content: center; + } + + // Add a subtle background when selected for better visibility + &.is-selected::before { + content: ''; + position: absolute; + top: -4px; + left: -4px; + right: -4px; + bottom: -4px; + background: rgba(0, 124, 186, 0.05); + border-radius: 2px; + z-index: -1; + } +} diff --git a/src/blocks/divider/index.js b/src/blocks/divider/index.js new file mode 100644 index 0000000..8e65121 --- /dev/null +++ b/src/blocks/divider/index.js @@ -0,0 +1,46 @@ +/** + * Divider Block - Registration + * + * Registers the Divider block with WordPress. + * Provides multiple separator styles for visual content division. + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import deprecated from './deprecated'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +/** + * Register Divider Block + * + * Visual separator with multiple style options including + * solid, dashed, gradient, and decorative patterns. + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, + deprecated, +}); diff --git a/src/blocks/divider/render.php b/src/blocks/divider/render.php new file mode 100644 index 0000000..3533cf2 --- /dev/null +++ b/src/blocks/divider/render.php @@ -0,0 +1,73 @@ + 'airo-wp-divider airo-wp-divider--' . sanitize_html_class( $divider_style ), + ) + ); + + $container_style = sprintf( 'width:%s%%;', $width ); + $line_style = sprintf( 'height:%dpx;', $thickness ); + + if ( 'icon' === $divider_style ) { + $defaults = class_exists( '\\airo-wp\\Icon_Injector' ) ? \airo-wp\Icon_Injector::get_icon_defaults() : array(); + $icon_name = ! empty( $attributes['iconName'] ) ? (string) $attributes['iconName'] : 'star'; + $icon_style = ! empty( $attributes['iconStyle'] ) ? (string) $attributes['iconStyle'] : ( isset( $defaults['style'] ) ? (string) $defaults['style'] : 'filled' ); + $stroke_width = isset( $attributes['strokeWidth'] ) ? (float) $attributes['strokeWidth'] : 1.5; + $svg = airowp_render_icon_svg( $icon_name, $icon_style, $stroke_width ); + + $inner = sprintf( + '
    %2$s
    ', + esc_attr( $line_style ), + $svg + ); + } else { + $inner = sprintf( '
    ', esc_attr( $line_style ) ); + } + + return sprintf( + '
    %3$s
    ', + $wrapper_attributes, + esc_attr( $container_style ), + $inner + ); + } +} + +// Output directly (WordPress captures echo'd output from render callbacks). +echo airowp_render_divider( $attributes, $content, $block ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped diff --git a/src/blocks/divider/save.js b/src/blocks/divider/save.js new file mode 100644 index 0000000..2e4adf7 --- /dev/null +++ b/src/blocks/divider/save.js @@ -0,0 +1,13 @@ +/** + * Divider Block - Save Function + * + * Dynamic block rendered server-side via render.php. Non-icon styles are pure + * CSS lines; the "icon" style embeds the SVG from the shared PHP icon library. + * No static HTML is saved to the database, so the serialized block is a single + * self-closing comment. Historical static markup is handled by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function DividerSave() { + return null; +} diff --git a/src/blocks/divider/style.scss b/src/blocks/divider/style.scss new file mode 100644 index 0000000..de7c655 --- /dev/null +++ b/src/blocks/divider/style.scss @@ -0,0 +1,293 @@ +/** + * Divider Block - Frontend & Editor Styles + * + * Multiple divider styles for broad site compatibility: + * - Solid, Dashed, Dotted, Double lines + * - Gradient fade effect + * - Decorative dots and wave patterns + * - Icon-centered separators (using real SVG icons from icon library) + * + * CRITICAL: Must be duplicated in editor.scss for editor/frontend parity + * + * @since 1.0.0 + */ + +.airo-wp-divider { + // Block Supports automatically applies color to this wrapper + // All child elements use currentColor to inherit the color + + // CRITICAL: Ensure divider takes full width of parent container + // Without this, divider can collapse to 0 width inside flex containers + width: 100%; + + // Container centers the divider (default) + &__container { + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; // Default center alignment + } + + // Left alignment + &.alignleft &__container { + margin-left: 0; + margin-right: auto; + justify-content: flex-start; + } + + // Right alignment + &.alignright &__container { + margin-left: auto; + margin-right: 0; + justify-content: flex-end; + } + + // Center alignment (explicit) + &.aligncenter &__container { + margin: 0 auto; + justify-content: center; + } + + // Base line styles + &__line { + width: 100%; + background-color: currentcolor; // Inherits from Block Supports + border: none; + } + + // ======================================== + // STYLE VARIATIONS + // ======================================== + + // 1. SOLID (Default) + &--solid { + + .airo-wp-divider__line { + background-color: currentcolor; + } + } + + // 2. DASHED + &--dashed { + + .airo-wp-divider__line { + background: none; + border-top: 2px dashed currentcolor; + height: 0 !important; // Override inline height + } + } + + // 3. DOTTED + &--dotted { + + .airo-wp-divider__line { + background: none; + border-top: 2px dotted currentcolor; + height: 0 !important; // Override inline height + } + } + + // 4. DOUBLE LINE + &--double { + + .airo-wp-divider__line { + background: none; + border-top: 3px double currentcolor; + height: 0 !important; // Override inline height + } + } + + // 5. GRADIENT FADE + &--gradient { + + .airo-wp-divider__line { + background: linear-gradient( + to right, + transparent, + currentcolor 20%, + currentcolor 80%, + transparent + ); + } + } + + // 6. DOTS PATTERN + &--dots { + + .airo-wp-divider__line { + background: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + &::before, + &::after { + content: ''; + width: 6px; + height: 6px; + background-color: currentcolor; + border-radius: 50%; + } + } + + // Add center dot + .airo-wp-divider__container::after { + content: ''; + position: absolute; + width: 6px; + height: 6px; + background-color: currentcolor; + border-radius: 50%; + left: 50%; + transform: translateX(-50%); + } + } + + // 7. WAVE PATTERN + &--wave { + + .airo-wp-divider__line { + background: none; + position: relative; + height: 4px !important; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background-image: repeating-linear-gradient( + 90deg, + currentcolor 0, + currentcolor 8px, + transparent 8px, + transparent 16px + ); + -webkit-mask-image: radial-gradient( + ellipse 100% 100% at 50% 100%, + transparent 40%, + black 41% + ); + mask-image: radial-gradient( + ellipse 100% 100% at 50% 100%, + transparent 40%, + black 41% + ); + } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 8px; + right: 0; + height: 100%; + background-image: repeating-linear-gradient( + 90deg, + currentcolor 0, + currentcolor 8px, + transparent 8px, + transparent 16px + ); + -webkit-mask-image: radial-gradient( + ellipse 100% 100% at 50% 0%, + transparent 40%, + black 41% + ); + mask-image: radial-gradient( + ellipse 100% 100% at 50% 0%, + transparent 40%, + black 41% + ); + } + } + } + + // 8. ICON CENTERED (Using real SVG icons) + &--icon { + + .airo-wp-divider__icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + width: 100%; + } + + .airo-wp-divider__line { + flex: 1; + background-color: currentcolor; + + &--left, + &--right { + height: 2px; + } + } + + .airo-wp-divider__icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: currentcolor; + + // SVG icon inherits color + svg { + width: 100%; + height: 100%; + fill: currentcolor; + } + } + } + + // ======================================== + // RESPONSIVE ADJUSTMENTS + // ======================================== + @media (max-width: 768px) { + + &--icon { + + .airo-wp-divider__icon-wrapper { + gap: 12px; + } + + .airo-wp-divider__icon { + width: 20px; + height: 20px; + } + } + + &--dots { + + .airo-wp-divider__line { + gap: 6px; + + &::before, + &::after { + width: 5px; + height: 5px; + } + } + + .airo-wp-divider__container::after { + width: 5px; + height: 5px; + } + } + } + + // ======================================== + // ACCESSIBILITY + // ======================================== + // Ensure sufficient contrast and proper color application + &[style*='color'] { + // Text color applied via Block Supports + // Divider line inherits via currentcolor + } +} diff --git a/src/blocks/dynamic-image/block.json b/src/blocks/dynamic-image/block.json new file mode 100644 index 0000000..6693182 --- /dev/null +++ b/src/blocks/dynamic-image/block.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/dynamic-image", + "version": "1.0.0", + "title": "Dynamic Image", + "category": "airo-wp", + "description": "An image block whose source resolves at render time from the post, site, user, or a custom field (ACF, Meta Box, Pods, JetEngine).", + "keywords": [ "image", "dynamic", "featured", "acf", "tag" ], + "textdomain": "airo-wp", + "icon": "format-image", + "usesContext": [ "postId", "postType" ], + "supports": { + "align": [ "wide", "full" ], + "anchor": true, + "spacing": { + "margin": true, + "padding": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + } + }, + "attributes": { + "source": { + "type": "string", + "default": "" + }, + "sourceArgs": { + "type": "object", + "default": {} + }, + "size": { + "type": "string", + "default": "full" + }, + "altOverride": { + "type": "string", + "default": "" + }, + "focalPoint": { + "type": "object", + "default": { "x": 0.5, "y": 0.5 } + }, + "aspectRatio": { + "type": "string", + "default": "" + }, + "objectFit": { + "type": "string", + "default": "cover" + }, + "fallbackId": { + "type": "number", + "default": 0 + }, + "fallbackUrl": { + "type": "string", + "default": "" + }, + "fallbackAlt": { + "type": "string", + "default": "" + }, + "href": { + "type": "string", + "default": "" + }, + "linkTarget": { + "type": "string", + "default": "" + }, + "rel": { + "type": "string", + "default": "" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./editor.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/dynamic-image/edit.js b/src/blocks/dynamic-image/edit.js new file mode 100644 index 0000000..26ea75a --- /dev/null +++ b/src/blocks/dynamic-image/edit.js @@ -0,0 +1,494 @@ +/** + * Dynamic Image block — editor. + * + * Authors pick a Dynamic Tag source (featured image, ACF image, + * site logo, etc.), optionally configure a size and focal point, + * and set a fallback image for when the source is empty. + * + * The editor preview hits REST /dynamic-tags/preview so what you see + * here matches what render.php will output on the frontend. + * + * Inspector follows the Theme 3 IA convention: a single canonical + * Settings ToolsPanel with one DsgoInspectorPanel.Item per attribute. + */ +import { + InspectorControls, + useBlockProps, + MediaUpload, + MediaUploadCheck, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { + Button, + SelectControl, + TextControl, + FocalPointPicker, + Placeholder, + Spinner, + __experimentalHStack as HStack, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { store as editorStore } from '@wordpress/editor'; + +import { + DynamicTagButton, + useDynamicTagPreview, +} from '../../components/DynamicTagPicker'; +import { DsgoInspectorPanel } from '../../components/shared'; + +const OBJECT_FIT_OPTIONS = [ + { label: __('Cover', 'airo-wp'), value: 'cover' }, + { label: __('Contain', 'airo-wp'), value: 'contain' }, + { label: __('Fill', 'airo-wp'), value: 'fill' }, + { label: __('Scale down', 'airo-wp'), value: 'scale-down' }, +]; + +const ASPECT_RATIO_OPTIONS = [ + { label: __('Original', 'airo-wp'), value: '' }, + { label: __('Square (1 : 1)', 'airo-wp'), value: '1/1' }, + { label: __('Landscape (16 : 9)', 'airo-wp'), value: '16/9' }, + { label: __('Landscape (4 : 3)', 'airo-wp'), value: '4/3' }, + { label: __('Landscape (3 : 2)', 'airo-wp'), value: '3/2' }, + { label: __('Portrait (3 : 4)', 'airo-wp'), value: '3/4' }, + { label: __('Portrait (2 : 3)', 'airo-wp'), value: '2/3' }, + { label: __('Portrait (9 : 16)', 'airo-wp'), value: '9/16' }, +]; + +const LINK_TARGET_OPTIONS = [ + { label: __('Same tab', 'airo-wp'), value: '' }, + { label: __('New tab', 'airo-wp'), value: '_blank' }, +]; + +const REL_OPTIONS = [ + { label: __('None', 'airo-wp'), value: '' }, + { label: 'nofollow', value: 'nofollow' }, + { label: 'noopener noreferrer', value: 'noopener noreferrer' }, + { + label: 'nofollow noopener noreferrer', + value: 'nofollow noopener noreferrer', + }, + { label: 'sponsored', value: 'sponsored' }, + { label: 'ugc', value: 'ugc' }, +]; + +const DEFAULTS = { + source: '', + sourceArgs: {}, + size: 'full', + altOverride: '', + focalPoint: { x: 0.5, y: 0.5 }, + aspectRatio: '', + objectFit: 'cover', + fallbackId: 0, + fallbackUrl: '', + fallbackAlt: '', + href: '', + linkTarget: '', + rel: '', +}; + +export default function Edit({ attributes, setAttributes, clientId, context }) { + const { + source, + sourceArgs, + size, + altOverride, + focalPoint, + aspectRatio, + objectFit, + fallbackId, + fallbackUrl, + fallbackAlt, + href, + linkTarget, + rel, + } = attributes; + + const { editorPostId, imageSizes } = useSelect((select) => { + const editor = select(editorStore); + const blockEditor = select(blockEditorStore); + return { + editorPostId: editor?.getCurrentPostId?.() || 0, + imageSizes: blockEditor?.getSettings?.()?.imageSizes || [], + }; + }, []); + const postId = context?.postId || editorPostId; + + const sizeOptions = imageSizes.length + ? imageSizes.map((s) => ({ label: s.name, value: s.slug })) + : [ + { label: __('Thumbnail', 'airo-wp'), value: 'thumbnail' }, + { label: __('Medium', 'airo-wp'), value: 'medium' }, + { label: __('Large', 'airo-wp'), value: 'large' }, + { label: __('Full', 'airo-wp'), value: 'full' }, + ]; + + // If a stored aspectRatio value isn't one of our presets (e.g. a + // custom 4/5 saved by an earlier free-text version of this control), + // surface it as an extra option so authors can still see and edit + // it instead of the dropdown silently snapping to "Original". + const aspectRatioOptions = useMemo(() => { + if ( + !aspectRatio || + ASPECT_RATIO_OPTIONS.some((opt) => opt.value === aspectRatio) + ) { + return ASPECT_RATIO_OPTIONS; + } + return [ + ...ASPECT_RATIO_OPTIONS, + { + label: sprintf( + /* translators: %s: custom aspect-ratio value such as 4/5 */ + __('Custom (%s)', 'airo-wp'), + aspectRatio + ), + value: aspectRatio, + }, + ]; + }, [aspectRatio]); + + const preview = useDynamicTagPreview({ + source, + args: sourceArgs, + postId, + size, + }); + + const blockProps = useBlockProps({ + style: aspectRatio ? { aspectRatio } : undefined, + }); + + const sourceValue = source ? { source, args: sourceArgs } : null; + + const handleSourceChange = (next) => { + if (!next) { + setAttributes({ source: '', sourceArgs: {} }); + return; + } + setAttributes({ source: next.source, sourceArgs: next.args || {} }); + }; + + const resolvedImage = + preview.status === 'resolved' && + preview.returns === 'image' && + preview.value + ? preview.value + : null; + + const displayImage = + resolvedImage || + (fallbackUrl + ? { url: fallbackUrl, alt: fallbackAlt || '', width: 0, height: 0 } + : null); + + const isDefaultFocalPoint = (fp) => !fp || (fp.x === 0.5 && fp.y === 0.5); + + return ( + <> + + setAttributes(DEFAULTS)} + > + source !== ''} + onDeselect={() => + setAttributes({ source: '', sourceArgs: {} }) + } + isShownByDefault + > + + {source && ( +

    + {source} +

    + )} +
    + + size !== 'full'} + onDeselect={() => setAttributes({ size: 'full' })} + isShownByDefault + > + setAttributes({ size: value })} + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + + altOverride !== ''} + onDeselect={() => setAttributes({ altOverride: '' })} + isShownByDefault + > + + setAttributes({ altOverride: value }) + } + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + + aspectRatio !== ''} + onDeselect={() => setAttributes({ aspectRatio: '' })} + isShownByDefault + > + + setAttributes({ aspectRatio: value }) + } + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + + objectFit !== 'cover'} + onDeselect={() => setAttributes({ objectFit: 'cover' })} + isShownByDefault + > + + setAttributes({ objectFit: value }) + } + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + + {displayImage?.url && ( + !isDefaultFocalPoint(focalPoint)} + onDeselect={() => + setAttributes({ + focalPoint: { x: 0.5, y: 0.5 }, + }) + } + isShownByDefault={false} + > + + setAttributes({ focalPoint: value }) + } + __nextHasNoMarginBottom + /> + + )} + + fallbackId !== 0 || fallbackUrl !== ''} + onDeselect={() => + setAttributes({ + fallbackId: 0, + fallbackUrl: '', + fallbackAlt: '', + }) + } + isShownByDefault + > +

    + {__( + 'Shown when the source is empty — e.g. a post without a featured image.', + 'airo-wp' + )} +

    + + + setAttributes({ + fallbackId: media.id, + fallbackUrl: media.url, + fallbackAlt: media.alt || '', + }) + } + allowedTypes={['image']} + value={fallbackId} + render={({ open }) => ( + + + {fallbackId && ( + + )} + + )} + /> + + {fallbackUrl && ( + + )} +
    + + href !== ''} + onDeselect={() => + setAttributes({ href: '', linkTarget: '', rel: '' }) + } + isShownByDefault={false} + > + setAttributes({ href: value })} + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + + {href && ( + linkTarget !== ''} + onDeselect={() => setAttributes({ linkTarget: '' })} + isShownByDefault={false} + > + + setAttributes({ linkTarget: value }) + } + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + )} + + {href && ( + rel !== ''} + onDeselect={() => setAttributes({ rel: '' })} + isShownByDefault={false} + > + + setAttributes({ rel: value }) + } + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + + )} +
    +
    + +
    + {!source && !fallbackUrl && ( + + + + )} + + {preview.status === 'loading' && source && !displayImage && ( +
    + +
    + )} + + {displayImage?.url && ( + {altOverride + )} + + {source && preview.status === 'empty' && !displayImage && ( +
    + {__( + 'Source is empty on this post. Add a fallback image to always show something.', + 'airo-wp' + )} +
    + )} +
    + + ); +} diff --git a/src/blocks/dynamic-image/editor.scss b/src/blocks/dynamic-image/editor.scss new file mode 100644 index 0000000..eafe0e0 --- /dev/null +++ b/src/blocks/dynamic-image/editor.scss @@ -0,0 +1,47 @@ +.wp-block-airo-wp-dynamic-image { + position: relative; + margin: 0; + + > img { + display: block; + max-width: 100%; + height: auto; + } + + .airo-wp-dynamic-image__loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 120px; + background: #f0f0f1; + } + + .airo-wp-dynamic-image__empty { + padding: 20px; + color: #757575; + font-style: italic; + background: #f6f7f7; + border: 1px dashed #ccc; + border-radius: 4px; + text-align: center; + } + + .airo-wp-dynamic-image__source-summary code { + font-size: 11px; + color: #757575; + } + + .airo-wp-dynamic-image__help { + margin-top: 0; + color: #757575; + font-size: 12px; + } + + .airo-wp-dynamic-image__fallback-preview { + margin-top: 12px; + max-width: 100%; + max-height: 120px; + display: block; + border-radius: 2px; + } +} diff --git a/src/blocks/dynamic-image/index.js b/src/blocks/dynamic-image/index.js new file mode 100644 index 0000000..c2ea437 --- /dev/null +++ b/src/blocks/dynamic-image/index.js @@ -0,0 +1,34 @@ +/** + * Dynamic Image block — registration. + */ +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; + +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + + ), + }, + edit, + save, +}); diff --git a/src/blocks/dynamic-image/render.php b/src/blocks/dynamic-image/render.php new file mode 100644 index 0000000..e0bf993 --- /dev/null +++ b/src/blocks/dynamic-image/render.php @@ -0,0 +1,151 @@ + (with optional
    / wrapping). Falls back to + * a fallback image when the source is empty. Never emits anything when + * both source and fallback are empty so authors can rely on it to hide + * gracefully on posts that don't have the field. + * + * @package airo-wp + * @since 2.2.0 + * + * @var array $attributes Block attributes. + * @var string $content Block save content (empty — this block is server-rendered). + * @var WP_Block $block Block instance. + */ + +defined( 'ABSPATH' ) || exit; + +use GoDaddy\WordPress\Plugins\AiroWp\Blocks\Common\DynamicTags\ImageResolver; + +if ( ! function_exists( 'airowp_render_dynamic_image' ) ) { + /** + * Render the Dynamic Image block. + * + * @param array $attributes Block attributes. + * @param string $content Inner block content. + * @param WP_Block $block Block instance. + * @return void + */ + function airowp_render_dynamic_image( $attributes, $content, $block ) { + $source = isset( $attributes['source'] ) ? (string) $attributes['source'] : ''; + $source_args = isset( $attributes['sourceArgs'] ) && is_array( $attributes['sourceArgs'] ) ? $attributes['sourceArgs'] : array(); + $size = isset( $attributes['size'] ) ? (string) $attributes['size'] : 'full'; + $alt_override = isset( $attributes['altOverride'] ) ? (string) $attributes['altOverride'] : ''; + $fallback_id = isset( $attributes['fallbackId'] ) ? (int) $attributes['fallbackId'] : 0; + $fallback_url = isset( $attributes['fallbackUrl'] ) ? (string) $attributes['fallbackUrl'] : ''; + $fallback_alt = isset( $attributes['fallbackAlt'] ) ? (string) $attributes['fallbackAlt'] : ''; + $focal = isset( $attributes['focalPoint'] ) && is_array( $attributes['focalPoint'] ) ? $attributes['focalPoint'] : array(); + $aspect_ratio = isset( $attributes['aspectRatio'] ) ? (string) $attributes['aspectRatio'] : ''; + $object_fit = isset( $attributes['objectFit'] ) ? (string) $attributes['objectFit'] : 'cover'; + $href = isset( $attributes['href'] ) ? (string) $attributes['href'] : ''; + $link_target = isset( $attributes['linkTarget'] ) ? (string) $attributes['linkTarget'] : ''; + $rel = isset( $attributes['rel'] ) ? (string) $attributes['rel'] : ''; + + $post_id = 0; + if ( isset( $block->context['postId'] ) ) { + $post_id = (int) $block->context['postId']; + } elseif ( get_the_ID() ) { + $post_id = (int) get_the_ID(); + } + + $descriptor = null; + if ( '' !== $source ) { + $descriptor = ImageResolver::resolve( $source, $source_args, $post_id, $size ); + } + + // Fallback lookup. + if ( null === $descriptor ) { + if ( $fallback_id ) { + $descriptor = ImageResolver::descriptor_from_id( $fallback_id, $size ); + } elseif ( '' !== $fallback_url ) { + $descriptor = array( + 'id' => 0, + 'url' => $fallback_url, + 'alt' => $fallback_alt, + 'width' => 0, + 'height' => 0, + ); + } + } + + if ( null === $descriptor || empty( $descriptor['url'] ) ) { + return ''; + } + + $alt = '' !== $alt_override ? $alt_override : (string) ( $descriptor['alt'] ?? '' ); + + $figure_style = array(); + if ( '' !== $aspect_ratio ) { + $figure_style[] = 'aspect-ratio:' . $aspect_ratio; + } + + $img_style = array( + 'object-fit:' . $object_fit, + ); + if ( isset( $focal['x'], $focal['y'] ) ) { + $img_style[] = sprintf( + 'object-position:%s%% %s%%', + round( (float) $focal['x'] * 100 ), + round( (float) $focal['y'] * 100 ) + ); + } + + $wrapper_attrs = get_block_wrapper_attributes( + array( + 'class' => 'wp-block-airo-wp-dynamic-image', + 'style' => implode( ';', $figure_style ), + ) + ); + + $img_attrs = array( + 'src' => esc_url( $descriptor['url'] ), + 'alt' => esc_attr( $alt ), + 'loading' => 'lazy', + 'decoding' => 'async', + 'style' => esc_attr( implode( ';', $img_style ) ), + ); + if ( ! empty( $descriptor['width'] ) ) { + $img_attrs['width'] = (int) $descriptor['width']; + } + if ( ! empty( $descriptor['height'] ) ) { + $img_attrs['height'] = (int) $descriptor['height']; + } + + $img_html = ' $val ) { + $img_html .= ' ' . $key . '="' . $val . '"'; + } + $img_html .= ' />'; + + if ( '' !== $href ) { + $link_attrs = array( + 'href' => esc_url( $href ), + ); + if ( '' !== $link_target ) { + $link_attrs['target'] = esc_attr( $link_target ); + } + if ( '' !== $rel ) { + $link_attrs['rel'] = esc_attr( $rel ); + } elseif ( '_blank' === $link_target ) { + $link_attrs['rel'] = 'noopener noreferrer'; + } + $link_open = ' $val ) { + $link_open .= ' ' . $key . '="' . $val . '"'; + } + $link_open .= '>'; + $img_html = $link_open . $img_html . ''; + } + + printf( + '
    %s
    ', + $wrapper_attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() output is pre-escaped. + $img_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- all attribute values above are esc_* escaped. + ); + } +} + +airowp_render_dynamic_image( $attributes, $content, $block ); diff --git a/src/blocks/dynamic-image/save.js b/src/blocks/dynamic-image/save.js new file mode 100644 index 0000000..ca85a7d --- /dev/null +++ b/src/blocks/dynamic-image/save.js @@ -0,0 +1,8 @@ +/** + * Dynamic Image block — save. + * + * Server-rendered via render.php, so save returns null. + */ +export default function save() { + return null; +} diff --git a/src/blocks/dynamic-image/style.scss b/src/blocks/dynamic-image/style.scss new file mode 100644 index 0000000..3c9eb91 --- /dev/null +++ b/src/blocks/dynamic-image/style.scss @@ -0,0 +1,19 @@ +.wp-block-airo-wp-dynamic-image { + margin: 0; + display: block; + + > img { + display: block; + max-width: 100%; + height: auto; + } + + &[style*="aspect-ratio"] { + + > img { + width: 100%; + height: 100%; + object-fit: cover; + } + } +} diff --git a/src/blocks/fifty-fifty/block.json b/src/blocks/fifty-fifty/block.json new file mode 100644 index 0000000..5fec344 --- /dev/null +++ b/src/blocks/fifty-fifty/block.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/fifty-fifty", + "version": "1.0.0", + "title": "Fifty Fifty", + "category": "airo-wp", + "description": "Full-width 50/50 split layout with edge-to-edge media on one side and constrained content on the other. Perfect for hero sections, feature highlights, and about sections.", + "keywords": [ + "fifty", + "fifty-fifty", + "split", + "half", + "media", + "image", + "hero" + ], + "textdomain": "airo-wp", + "icon": "columns", + "supports": { + "anchor": true, + "align": [ + "full" + ], + "html": false, + "inserter": true, + "color": { + "background": true, + "text": true, + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": false, + "blockGap": true, + "__experimentalDefaultControls": { + "blockGap": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "attributes": { + "align": { + "type": "string", + "default": "full" + }, + "mediaPosition": { + "type": "string", + "default": "left", + "enum": [ + "left", + "right" + ] + }, + "mediaId": { + "type": "number", + "default": 0 + }, + "mediaUrl": { + "type": "string", + "default": "" + }, + "mediaAlt": { + "type": "string", + "default": "" + }, + "focalPoint": { + "type": "object", + "default": { + "x": 0.5, + "y": 0.5 + } + }, + "minHeight": { + "type": "string", + "default": "500px" + }, + "verticalAlignment": { + "type": "string", + "default": "center", + "enum": [ + "top", + "center", + "bottom" + ] + }, + "contentPadding": { + "type": "string", + "default": "var:preset|spacing|50" + } + }, + "schemaMetadata": { + "attributeDescriptions": { + "align": "Block alignment (always full width)", + "mediaPosition": "Which side the media appears on (left or right)", + "mediaId": "WordPress media library attachment ID for the image", + "mediaUrl": "URL of the media image", + "mediaAlt": "Alt text for the media image", + "focalPoint": "Focal point for the media image (x, y coordinates from 0 to 1)", + "minHeight": "Minimum height of the block (CSS value)", + "verticalAlignment": "Vertical alignment of content within the content side (top, center, bottom)", + "contentPadding": "Internal padding for the content side" + } + }, + "example": { + "attributes": { + "mediaPosition": "left", + "minHeight": "500px" + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "level": 2, + "content": "A Bold Statement" + } + }, + { + "name": "core/paragraph", + "attributes": { + "content": "Pair striking imagery with compelling content in a balanced 50/50 layout that commands attention." + } + } + ] + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/src/blocks/fifty-fifty/edit.js b/src/blocks/fifty-fifty/edit.js new file mode 100644 index 0000000..75d32d3 --- /dev/null +++ b/src/blocks/fifty-fifty/edit.js @@ -0,0 +1,472 @@ +/** + * Fifty Fifty Block - Edit Component + * + * Full-width 50/50 split with edge-to-edge media and constrained content. + * + * @since 1.5.0 + */ + +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InnerBlocks, + InspectorControls, + MediaUpload, + MediaUploadCheck, + MediaReplaceFlow, + BlockControls, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { + SelectControl, + FocalPointPicker, + Button, + TextControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseCustomUnits as useCustomUnits, + ToolbarButton, + ToolbarGroup, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useSelect } from '@wordpress/data'; +import { convertPresetToCSSVar } from '../../utils/convert-preset-to-css-var'; + +/** + * Fifty Fifty Edit Component + * + * @param {Object} props Component props + * @param {Object} props.attributes Block attributes + * @param {Function} props.setAttributes Function to set attributes + * @param {string} props.clientId Block client ID + * @return {JSX.Element} Edit component + */ +export default function FiftyFiftyEdit({ + attributes, + setAttributes, + clientId, +}) { + const { + mediaPosition, + mediaId, + mediaUrl, + mediaAlt, + focalPoint, + minHeight, + verticalAlignment, + contentPadding, + } = attributes; + + // Units for min height control + const units = useCustomUnits({ + availableUnits: ['px', 'vh', 'vw', 'em', 'rem'], + }); + + // Check if block has inner blocks for appender logic + const { hasInnerBlocks } = useSelect( + (select) => { + const { getBlock } = select(blockEditorStore); + const block = getBlock(clientId); + return { + hasInnerBlocks: block?.innerBlocks?.length > 0, + }; + }, + [clientId] + ); + + // Map verticalAlignment to CSS align-items + const alignItemsMap = { + top: 'flex-start', + center: 'center', + bottom: 'flex-end', + }; + + // Build block class name + const blockClassName = [ + 'airo-wp-fifty-fifty', + `airo-wp-fifty-fifty--media-${mediaPosition}`, + ].join(' '); + + // Block wrapper props + const blockProps = useBlockProps({ + className: blockClassName, + style: { + '--airo-wp-fifty-fifty-min-height': minHeight || undefined, + '--airo-wp-fifty-fifty-content-justify': + alignItemsMap[verticalAlignment] || 'center', + '--airo-wp-fifty-fifty-content-padding': + convertPresetToCSSVar(contentPadding) || undefined, + }, + }); + + // InnerBlocks props - placed in the content-inner wrapper + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-fifty-fifty__content-inner', + }, + { + template: [ + [ + 'core/heading', + { + level: 2, + placeholder: __('Add heading…', 'airo-wp'), + }, + ], + [ + 'core/paragraph', + { + placeholder: __('Add content…', 'airo-wp'), + }, + ], + ], + templateLock: false, + renderAppender: hasInnerBlocks + ? undefined + : InnerBlocks.ButtonBlockAppender, + } + ); + + // Media selection handler + const onSelectMedia = (media) => { + setAttributes({ + mediaId: media.id, + mediaUrl: media.url, + mediaAlt: media.alt || '', + }); + }; + + const onRemoveMedia = () => { + setAttributes({ + mediaId: 0, + mediaUrl: '', + mediaAlt: '', + focalPoint: { x: 0.5, y: 0.5 }, + }); + }; + + // Focal point as object-position (coerce to Number to prevent CSS injection) + const objectPosition = focalPoint + ? `${Number(focalPoint.x) * 100}% ${Number(focalPoint.y) * 100}%` + : '50% 50%'; + + return ( + <> + {/* Toolbar: flip media side + replace media */} + + + + setAttributes({ + mediaPosition: + mediaPosition === 'left' ? 'right' : 'left', + }) + } + /> + + {mediaUrl && ( + + )} + + + {/* Inspector Controls */} + + + setAttributes({ + mediaPosition: 'left', + verticalAlignment: 'center', + minHeight: '500px', + mediaId: 0, + mediaUrl: '', + mediaAlt: '', + focalPoint: { x: 0.5, y: 0.5 }, + }) + } + > + mediaPosition !== 'left'} + onDeselect={() => + setAttributes({ mediaPosition: 'left' }) + } + isShownByDefault + > + + setAttributes({ mediaPosition: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + verticalAlignment !== 'center'} + onDeselect={() => + setAttributes({ verticalAlignment: 'center' }) + } + isShownByDefault + > + + setAttributes({ verticalAlignment: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + minHeight !== '500px'} + onDeselect={() => setAttributes({ minHeight: '500px' })} + isShownByDefault + > + + setAttributes({ minHeight: value }) + } + units={units} + __next40pxDefaultSize + /> + + + !!mediaUrl} + onDeselect={() => + setAttributes({ + mediaId: 0, + mediaUrl: '', + mediaAlt: '', + focalPoint: { x: 0.5, y: 0.5 }, + }) + } + isShownByDefault + > + + ( + <> + {mediaUrl ? ( + <> + {mediaAlt} +
    + + +
    + + ) : ( + + )} + + )} + /> +
    +
    + + {mediaUrl && ( + mediaAlt !== ''} + onDeselect={() => setAttributes({ mediaAlt: '' })} + isShownByDefault + > + + setAttributes({ mediaAlt: value }) + } + help={__( + 'Describe the image for accessibility.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {mediaUrl && ( + + focalPoint?.x !== 0.5 || focalPoint?.y !== 0.5 + } + onDeselect={() => + setAttributes({ + focalPoint: { x: 0.5, y: 0.5 }, + }) + } + isShownByDefault + > + + setAttributes({ focalPoint: value }) + } + help={__( + 'Click to adjust which part of the image stays visible.', + 'airo-wp' + )} + /> + + )} +
    +
    + + {/* Block Output */} +
    +
    + {mediaUrl ? ( + {mediaAlt} + ) : ( + + ( +
    { + if ( + e.key === 'Enter' || + e.key === ' ' + ) { + e.preventDefault(); + open(); + } + }} + role="button" + tabIndex={0} + aria-label={__( + 'Select image', + 'airo-wp' + )} + > + + + {__('Select Image', 'airo-wp')} + +
    + )} + /> +
    + )} +
    + +
    +
    +
    +
    + + ); +} diff --git a/src/blocks/fifty-fifty/editor.scss b/src/blocks/fifty-fifty/editor.scss new file mode 100644 index 0000000..96a4eff --- /dev/null +++ b/src/blocks/fifty-fifty/editor.scss @@ -0,0 +1,30 @@ +/** + * Fifty Fifty Block - Editor Styles + * + * Editor-specific overrides for the 50/50 split block. + * The frontend style.scss is also loaded in the editor; + * this file only contains editor-specific additions. + * + * @package airo-wp + * @since 1.5.0 + */ + +.airo-wp-fifty-fifty { + // Placeholder: hover/focus styling for accessibility in the editor + &__media-placeholder { + transition: background-color 0.15s ease; + + &:hover, + &:focus { + background-color: #e0e0e0; + outline: 2px solid var(--wp-admin-theme-color, #007cba); + outline-offset: -2px; + } + } + + // Block appender positioning within content area + .airo-wp-fifty-fifty__content-inner > .block-list-appender:not(:only-child) { + position: relative; + width: 100%; + } +} diff --git a/src/blocks/fifty-fifty/index.js b/src/blocks/fifty-fifty/index.js new file mode 100644 index 0000000..582ffab --- /dev/null +++ b/src/blocks/fifty-fifty/index.js @@ -0,0 +1,83 @@ +/** + * Fifty Fifty Block Registration + * + * Full-width 50/50 split with edge-to-edge media and constrained content. + * + * @since 1.5.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +/** + * Register Fifty Fifty Block + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + {/* Left half: media (filled rectangle) */} + + {/* Right half: content (outlined rectangle with text lines) */} + + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, +}); diff --git a/src/blocks/fifty-fifty/save.js b/src/blocks/fifty-fifty/save.js new file mode 100644 index 0000000..45545ce --- /dev/null +++ b/src/blocks/fifty-fifty/save.js @@ -0,0 +1,91 @@ +/** + * Fifty Fifty Block - Save Component + * + * Saves full-width 50/50 split with edge-to-edge media and constrained content. + * + * @since 1.5.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { convertPresetToCSSVar } from '../../utils/convert-preset-to-css-var'; +import { isValidImageUrl } from '../../utils/is-valid-image-url'; + +/** + * Fifty Fifty Save Component + * + * @param {Object} props Component props + * @param {Object} props.attributes Block attributes + * @return {JSX.Element} Save component + */ +export default function FiftyFiftySave({ attributes }) { + const { + mediaPosition, + mediaUrl, + mediaAlt, + focalPoint, + minHeight, + verticalAlignment, + contentPadding, + } = attributes; + + // Map verticalAlignment to CSS + const alignItemsMap = { + top: 'flex-start', + center: 'center', + bottom: 'flex-end', + }; + + // Validate mediaPosition to prevent class name injection + const safeMediaPosition = mediaPosition === 'right' ? 'right' : 'left'; + + const blockClassName = [ + 'airo-wp-fifty-fifty', + `airo-wp-fifty-fifty--media-${safeMediaPosition}`, + ].join(' '); + + // Sanitize minHeight: only allow valid CSS length values + const safeMinHeight = + minHeight && /^[\d.]+(px|vh|vw|em|rem|%)$/.test(minHeight) + ? minHeight + : undefined; + + const blockProps = useBlockProps.save({ + className: blockClassName, + style: { + '--airo-wp-fifty-fifty-min-height': safeMinHeight, + '--airo-wp-fifty-fifty-content-justify': + alignItemsMap[verticalAlignment] || 'center', + '--airo-wp-fifty-fifty-content-padding': + convertPresetToCSSVar(contentPadding) || undefined, + }, + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-fifty-fifty__content-inner', + }); + + // Focal point as object-position (coerce to Number to prevent CSS injection) + const objectPosition = focalPoint + ? `${Number(focalPoint.x) * 100}% ${Number(focalPoint.y) * 100}%` + : undefined; + + return ( +
    +
    + {mediaUrl && isValidImageUrl(mediaUrl) && ( + {mediaAlt + )} +
    + +
    +
    +
    +
    + ); +} diff --git a/src/blocks/fifty-fifty/style.scss b/src/blocks/fifty-fifty/style.scss new file mode 100644 index 0000000..69be117 --- /dev/null +++ b/src/blocks/fifty-fifty/style.scss @@ -0,0 +1,173 @@ +/** + * Fifty Fifty Block - Frontend Styles + * + * Full-width 50/50 split layout with edge-to-edge media. + * + * Key technique: The content side uses dynamic padding to align + * its inner content with the site's content-width boundary. + * When the block spans 100vw, each half is 50vw. The outer-edge + * padding on the content side equals max(base-padding, viewport-margin) + * so text aligns where normal page content starts. + * + * @package airo-wp + * @since 1.5.0 + */ + +.airo-wp-fifty-fifty { + box-sizing: border-box; + + // Two-column grid: each half is 50% of the container + display: grid; + grid-template-columns: 1fr 1fr; + min-height: var(--airo-wp-fifty-fifty-min-height, auto); + + // No gap between the two halves + gap: 0; + + // Prevent overflow from full-bleed images + overflow: hidden; + + // ============================================== + // MEDIA SIDE + // ============================================== + &__media { + position: relative; + overflow: hidden; + + img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + } + + // ============================================== + // CONTENT SIDE + // ============================================== + &__content { + display: flex; + flex-direction: column; + justify-content: var(--airo-wp-fifty-fifty-content-justify, center); + + // Vertical padding for breathing room + padding-top: var(--airo-wp-fifty-fifty-content-padding, var(--wp--preset--spacing--50, 2rem)); + padding-bottom: var(--airo-wp-fifty-fifty-content-padding, var(--wp--preset--spacing--50, 2rem)); + } + + // Content inner - width constraint technique: + // outer-edge padding = max(base, (100vw - contentWidth) / 2) + // inner wrapper max-width = contentWidth / 2 + &__content-inner { + width: 100%; + max-width: calc(var(--wp--style--global--content-size, 1140px) / 2); + } + + // ----- Media Left: content on right ----- + // Content's inner edge faces the media (left side). + // Content's outer edge faces the right viewport edge. + &--media-left &__content { + // Inner edge: comfortable padding + padding-left: var(--wp--preset--spacing--50, 2rem); + // Outer edge: match the viewport margin + padding-right: max( + var(--wp--preset--spacing--50, 2rem), + calc((100vw - var(--wp--style--global--content-size, 1140px)) / 2) + ); + } + + // Inner wrapper hugs the inner edge (left) for media-left + &--media-left &__content-inner { + margin-right: auto; + margin-left: 0; + } + + // ----- Media Right: content on left ----- + // Swap the grid order so media visually appears on the right. + &--media-right { + + .airo-wp-fifty-fifty__media { + order: 2; + } + + .airo-wp-fifty-fifty__content { + order: 1; + // Outer edge: left side faces viewport edge + padding-left: max( + var(--wp--preset--spacing--50, 2rem), + calc((100vw - var(--wp--style--global--content-size, 1140px)) / 2) + ); + // Inner edge: comfortable padding + padding-right: var(--wp--preset--spacing--50, 2rem); + } + + // Inner wrapper hugs the inner edge (right) for media-right + .airo-wp-fifty-fifty__content-inner { + margin-left: auto; + margin-right: 0; + } + } + + // ============================================== + // MEDIA PLACEHOLDER (editor only) + // ============================================== + &__media-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-height: 300px; + background-color: #f0f0f0; + color: #757575; + cursor: pointer; + gap: 8px; + font-size: 14px; + + .dashicons { + font-size: 48px; + width: 48px; + height: 48px; + } + + &:hover { + background-color: #e0e0e0; + } + } + + // ============================================== + // MOBILE RESPONSIVE + // ============================================== + @media (max-width: 767px) { + // Stack vertically + grid-template-columns: 1fr; + + // Remove content-width constraint on mobile - full width + .airo-wp-fifty-fifty__content-inner { + max-width: none; + margin-left: auto; + margin-right: auto; + } + } + + // Use increased specificity to override media-left/media-right + // order and padding without !important + &.airo-wp-fifty-fifty { + + @media (max-width: 767px) { + // Media always comes first on mobile regardless of mediaPosition + .airo-wp-fifty-fifty__media { + order: 1; + min-height: 250px; + } + + .airo-wp-fifty-fifty__content { + order: 2; + // Reset to standard mobile padding + padding-left: var(--wp--preset--spacing--30, 1rem); + padding-right: var(--wp--preset--spacing--30, 1rem); + } + } + } +} diff --git a/src/blocks/flip-card-back/block.json b/src/blocks/flip-card-back/block.json new file mode 100644 index 0000000..e00da50 --- /dev/null +++ b/src/blocks/flip-card-back/block.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/flip-card-back", + "version": "1.0.0", + "title": "Flip Card Back", + "category": "airo-wp", + "parent": [ + "airo-wp/flip-card" + ], + "description": "Back face of the flip card.", + "keywords": [ + "flip", + "card", + "back" + ], + "textdomain": "airo-wp", + "icon": "id-alt", + "supports": { + "anchor": false, + "align": false, + "html": false, + "inserter": false, + "reusable": false, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowEditing": true, + "allowVerticalAlignment": true, + "allowJustification": true, + "default": { + "type": "flex", + "orientation": "vertical", + "verticalAlignment": "center", + "justifyContent": "center" + } + }, + "spacing": { + "padding": true, + "margin": false, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true + } + } + }, + "attributes": {}, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/src/blocks/flip-card-back/edit.js b/src/blocks/flip-card-back/edit.js new file mode 100644 index 0000000..c93ddcd --- /dev/null +++ b/src/blocks/flip-card-back/edit.js @@ -0,0 +1,37 @@ +/** + * Flip Card Back - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +export default function FlipCardBackEdit() { + const blockProps = useBlockProps({ + className: 'airo-wp-flip-card__face airo-wp-flip-card__back', + }); + + const innerBlocksProps = useInnerBlocksProps(blockProps, { + template: [ + [ + 'core/heading', + { + content: __('Back of Card', 'airo-wp'), + level: 2, + textAlign: 'center', + }, + ], + [ + 'core/paragraph', + { + content: __('Add any blocks you want here…', 'airo-wp'), + align: 'center', + }, + ], + ], + templateLock: false, + }); + + return
    ; +} diff --git a/src/blocks/flip-card-back/editor.scss b/src/blocks/flip-card-back/editor.scss new file mode 100644 index 0000000..6982bf0 --- /dev/null +++ b/src/blocks/flip-card-back/editor.scss @@ -0,0 +1,8 @@ +/** + * Flip Card Back - Editor Styles + * + * @since 1.0.0 + */ + +// Styles are inherited from parent flip-card block +// This file is required for the build process diff --git a/src/blocks/flip-card-back/index.js b/src/blocks/flip-card-back/index.js new file mode 100644 index 0000000..87b2893 --- /dev/null +++ b/src/blocks/flip-card-back/index.js @@ -0,0 +1,66 @@ +/** + * Flip Card Back Block Registration + * + * Deprecated in 2.0.51 in favour of airo-wp/flip-card-face. The block + * stays registered with inserter:false so existing content keeps rendering; + * the transforms.to entry lets editors one-click convert it to the new + * consolidated block. + * + * @since 1.0.0 + */ + +import { registerBlockType, createBlock } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, + transforms: { + to: [ + { + type: 'block', + blocks: ['airo-wp/flip-card-face'], + transform: (attributes, innerBlocks) => + createBlock( + 'airo-wp/flip-card-face', + { ...attributes, side: 'back' }, + innerBlocks + ), + }, + ], + }, +}); diff --git a/src/blocks/flip-card-back/save.js b/src/blocks/flip-card-back/save.js new file mode 100644 index 0000000..5482ab1 --- /dev/null +++ b/src/blocks/flip-card-back/save.js @@ -0,0 +1,17 @@ +/** + * Flip Card Back - Save Component + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +export default function FlipCardBackSave() { + const blockProps = useBlockProps.save({ + className: 'airo-wp-flip-card__face airo-wp-flip-card__back', + }); + + const innerBlocksProps = useInnerBlocksProps.save(blockProps); + + return
    ; +} diff --git a/src/blocks/flip-card-back/style.scss b/src/blocks/flip-card-back/style.scss new file mode 100644 index 0000000..38d0b81 --- /dev/null +++ b/src/blocks/flip-card-back/style.scss @@ -0,0 +1,8 @@ +/** + * Flip Card Back - Frontend Styles + * + * @since 1.0.0 + */ + +// Styles are inherited from parent flip-card block +// This file is required for the build process diff --git a/src/blocks/flip-card-face/block.json b/src/blocks/flip-card-face/block.json new file mode 100644 index 0000000..4d83b7f --- /dev/null +++ b/src/blocks/flip-card-face/block.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/flip-card-face", + "version": "1.0.0", + "title": "Flip Card Face", + "category": "design", + "parent": [ + "airo-wp/flip-card" + ], + "description": "A face of the flip card. Use the Side attribute to mark it as front or back.", + "keywords": [ + "flip", + "card", + "face", + "front", + "back" + ], + "textdomain": "airo-wp", + "supports": { + "anchor": false, + "align": false, + "html": false, + "inserter": true, + "reusable": false, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowEditing": true, + "allowVerticalAlignment": true, + "allowJustification": true, + "default": { + "type": "flex", + "orientation": "vertical", + "verticalAlignment": "center", + "justifyContent": "center" + } + }, + "spacing": { + "padding": true, + "margin": false, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true + } + } + }, + "attributes": { + "side": { + "type": "string", + "default": "front", + "enum": [ + "front", + "back" + ] + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/src/blocks/flip-card-face/edit.js b/src/blocks/flip-card-face/edit.js new file mode 100644 index 0000000..dc917c0 --- /dev/null +++ b/src/blocks/flip-card-face/edit.js @@ -0,0 +1,192 @@ +/** + * Flip Card Face - Edit Component + * + * Replaces the separate flip-card-front and flip-card-back blocks. The + * `side` attribute (front|back) controls placement inside the parent + * flip card. Kept as a child-only block so it can only appear inside + * airo-wp/flip-card. + * + * @since 2.0.51 + */ + +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, +} from '@wordpress/block-editor'; +import { Notice, SelectControl } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { DsgoInspectorPanel } from '../../components/shared'; + +export default function FlipCardFaceEdit({ + attributes, + setAttributes, + clientId, +}) { + const side = attributes.side === 'back' ? 'back' : 'front'; + + // The parent flip-card's view script and stylesheet assume exactly one + // front face and one back face — a duplicate side (two fronts, or the + // front face renamed to back while a sibling already holds back) would + // break the flip animation. Track which side(s) siblings occupy so we + // can disable the matching option in the Side dropdown and fall back to + // a Notice if legacy content already violates the invariant. We split + // siblings by position — `earlier` siblings are the ones we treat as + // "already present" when deciding whether this face is a newly inserted + // duplicate, while `all` drives the inspector dropdown's occupied-side + // state (which must consider every sibling regardless of order). + const { earlierSiblingSides, allSiblingSides } = useSelect( + (select) => { + const { getBlockRootClientId, getBlock } = + select('core/block-editor'); + const parentId = getBlockRootClientId(clientId); + if (!parentId) { + return { earlierSiblingSides: [], allSiblingSides: [] }; + } + const siblings = getBlock(parentId)?.innerBlocks || []; + const toSide = (sibling) => { + if (sibling.name === 'airo-wp/flip-card-face') { + return sibling.attributes?.side === 'back' + ? 'back' + : 'front'; + } + if (sibling.name === 'airo-wp/flip-card-front') { + return 'front'; + } + if (sibling.name === 'airo-wp/flip-card-back') { + return 'back'; + } + return null; + }; + const selfIndex = siblings.findIndex( + (sibling) => sibling.clientId === clientId + ); + const earlier = siblings + .slice(0, selfIndex === -1 ? 0 : selfIndex) + .map(toSide) + .filter(Boolean); + const all = siblings + .filter((sibling) => sibling.clientId !== clientId) + .map(toSide) + .filter(Boolean); + return { earlierSiblingSides: earlier, allSiblingSides: all }; + }, + [clientId] + ); + const siblingSides = allSiblingSides; + const hasDuplicateSide = siblingSides.includes(side); + + // When an author deletes a face and inserts a replacement via the parent's + // inserter, the new block uses the block.json default `side: 'front'`. If + // an earlier sibling already occupies this face's side and the opposite + // side is free, flip *this* face so the parent keeps exactly one front + // and one back. Using *earlier* siblings (rather than all siblings) is + // the tie-breaker: without it, two faces with the same side would each + // try to switch in the same tick and the older face would usually win. + // Only auto-correct when the opposite side is actually available — + // otherwise leave the duplicate in place so the Notice guides the author. + useEffect(() => { + if (!earlierSiblingSides.includes(side)) { + return; + } + const oppositeSide = side === 'front' ? 'back' : 'front'; + if (!siblingSides.includes(oppositeSide)) { + setAttributes({ side: oppositeSide }); + } + }, [earlierSiblingSides, siblingSides, side, setAttributes]); + + const blockProps = useBlockProps({ + className: `airo-wp-flip-card__face airo-wp-flip-card__${side}`, + }); + + // Template is seeded once on first insertion; switching `side` later does + // not re-template, so a face relabelled "back" will still show the + // original "Front of Card" placeholder heading until the author edits it. + const innerBlocksProps = useInnerBlocksProps(blockProps, { + template: [ + [ + 'core/heading', + { + content: + side === 'back' + ? __('Back of Card', 'airo-wp') + : __('Front of Card', 'airo-wp'), + level: 2, + textAlign: 'center', + }, + ], + [ + 'core/paragraph', + { + content: __('Add any blocks you want here…', 'airo-wp'), + align: 'center', + }, + ], + ], + templateLock: false, + }); + + return ( + <> + + setAttributes({ side: 'front' })} + > + side !== 'front'} + onDeselect={() => setAttributes({ side: 'front' })} + isShownByDefault + > + setAttributes({ side: value })} + help={__( + 'Choose whether this face shows on the front or back of the flip card.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + {hasDuplicateSide && ( + + {side === 'back' + ? __( + 'Another face on this flip card is already set to Back. Change one of them to Front so the card can flip.', + 'airo-wp' + ) + : __( + 'Another face on this flip card is already set to Front. Change one of them to Back so the card can flip.', + 'airo-wp' + )} + + )} + + + +
    + + ); +} diff --git a/src/blocks/flip-card-face/editor.scss b/src/blocks/flip-card-face/editor.scss new file mode 100644 index 0000000..1fa4f1a --- /dev/null +++ b/src/blocks/flip-card-face/editor.scss @@ -0,0 +1,8 @@ +/** + * Flip Card Face - Editor Styles + * + * @since 2.0.51 + */ + +// Styles are inherited from parent flip-card block +// This file is required for the build process diff --git a/src/blocks/flip-card-face/index.js b/src/blocks/flip-card-face/index.js new file mode 100644 index 0000000..4b91e63 --- /dev/null +++ b/src/blocks/flip-card-face/index.js @@ -0,0 +1,73 @@ +/** + * Flip Card Face Block Registration + * + * @since 2.0.51 + */ + +import { registerBlockType, createBlock } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, + // Spread preserves core-managed attrs (style, className, color, etc.) + // across the transform. flip-card-front / flip-card-back defined no + // custom attributes, so this is safe today — if either legacy block ever + // picks up a custom attribute, audit the spread before shipping. + transforms: { + from: [ + { + type: 'block', + blocks: ['airo-wp/flip-card-front'], + transform: (attributes, innerBlocks) => + createBlock( + 'airo-wp/flip-card-face', + { ...attributes, side: 'front' }, + innerBlocks + ), + }, + { + type: 'block', + blocks: ['airo-wp/flip-card-back'], + transform: (attributes, innerBlocks) => + createBlock( + 'airo-wp/flip-card-face', + { ...attributes, side: 'back' }, + innerBlocks + ), + }, + ], + }, +}); diff --git a/src/blocks/flip-card-face/save.js b/src/blocks/flip-card-face/save.js new file mode 100644 index 0000000..16538ea --- /dev/null +++ b/src/blocks/flip-card-face/save.js @@ -0,0 +1,19 @@ +/** + * Flip Card Face - Save Component + * + * @since 2.0.51 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +export default function FlipCardFaceSave({ attributes }) { + const side = attributes.side === 'back' ? 'back' : 'front'; + + const blockProps = useBlockProps.save({ + className: `airo-wp-flip-card__face airo-wp-flip-card__${side}`, + }); + + const innerBlocksProps = useInnerBlocksProps.save(blockProps); + + return
    ; +} diff --git a/src/blocks/flip-card-face/style.scss b/src/blocks/flip-card-face/style.scss new file mode 100644 index 0000000..1a8e9f7 --- /dev/null +++ b/src/blocks/flip-card-face/style.scss @@ -0,0 +1,8 @@ +/** + * Flip Card Face - Frontend Styles + * + * @since 2.0.51 + */ + +// Styles are inherited from parent flip-card block +// This file is required for the build process diff --git a/src/blocks/flip-card-front/block.json b/src/blocks/flip-card-front/block.json new file mode 100644 index 0000000..0977b83 --- /dev/null +++ b/src/blocks/flip-card-front/block.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/flip-card-front", + "version": "1.0.0", + "title": "Flip Card Front", + "category": "airo-wp", + "parent": [ + "airo-wp/flip-card" + ], + "description": "Front face of the flip card.", + "keywords": [ + "flip", + "card", + "front" + ], + "textdomain": "airo-wp", + "icon": "id", + "supports": { + "anchor": false, + "align": false, + "html": false, + "inserter": false, + "reusable": false, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowEditing": true, + "allowVerticalAlignment": true, + "allowJustification": true, + "default": { + "type": "flex", + "orientation": "vertical", + "verticalAlignment": "center", + "justifyContent": "center" + } + }, + "spacing": { + "padding": true, + "margin": false, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "radius": true + } + } + }, + "attributes": {}, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/src/blocks/flip-card-front/edit.js b/src/blocks/flip-card-front/edit.js new file mode 100644 index 0000000..e98962d --- /dev/null +++ b/src/blocks/flip-card-front/edit.js @@ -0,0 +1,37 @@ +/** + * Flip Card Front - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +export default function FlipCardFrontEdit() { + const blockProps = useBlockProps({ + className: 'airo-wp-flip-card__face airo-wp-flip-card__front', + }); + + const innerBlocksProps = useInnerBlocksProps(blockProps, { + template: [ + [ + 'core/heading', + { + content: __('Front of Card', 'airo-wp'), + level: 2, + textAlign: 'center', + }, + ], + [ + 'core/paragraph', + { + content: __('Add any blocks you want here…', 'airo-wp'), + align: 'center', + }, + ], + ], + templateLock: false, + }); + + return
    ; +} diff --git a/src/blocks/flip-card-front/editor.scss b/src/blocks/flip-card-front/editor.scss new file mode 100644 index 0000000..7d3f343 --- /dev/null +++ b/src/blocks/flip-card-front/editor.scss @@ -0,0 +1,8 @@ +/** + * Flip Card Front - Editor Styles + * + * @since 1.0.0 + */ + +// Styles are inherited from parent flip-card block +// This file is required for the build process diff --git a/src/blocks/flip-card-front/index.js b/src/blocks/flip-card-front/index.js new file mode 100644 index 0000000..e90f094 --- /dev/null +++ b/src/blocks/flip-card-front/index.js @@ -0,0 +1,64 @@ +/** + * Flip Card Front Block Registration + * + * Deprecated in 2.0.51 in favour of airo-wp/flip-card-face. The block + * stays registered with inserter:false so existing content keeps rendering; + * the transforms.to entry lets editors one-click convert it to the new + * consolidated block. + * + * @since 1.0.0 + */ + +import { registerBlockType, createBlock } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, + transforms: { + to: [ + { + type: 'block', + blocks: ['airo-wp/flip-card-face'], + transform: (attributes, innerBlocks) => + createBlock( + 'airo-wp/flip-card-face', + { ...attributes, side: 'front' }, + innerBlocks + ), + }, + ], + }, +}); diff --git a/src/blocks/flip-card-front/save.js b/src/blocks/flip-card-front/save.js new file mode 100644 index 0000000..cf0d18a --- /dev/null +++ b/src/blocks/flip-card-front/save.js @@ -0,0 +1,17 @@ +/** + * Flip Card Front - Save Component + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +export default function FlipCardFrontSave() { + const blockProps = useBlockProps.save({ + className: 'airo-wp-flip-card__face airo-wp-flip-card__front', + }); + + const innerBlocksProps = useInnerBlocksProps.save(blockProps); + + return
    ; +} diff --git a/src/blocks/flip-card-front/style.scss b/src/blocks/flip-card-front/style.scss new file mode 100644 index 0000000..646fdcd --- /dev/null +++ b/src/blocks/flip-card-front/style.scss @@ -0,0 +1,8 @@ +/** + * Flip Card Front - Frontend Styles + * + * @since 1.0.0 + */ + +// Styles are inherited from parent flip-card block +// This file is required for the build process diff --git a/src/blocks/flip-card/block.json b/src/blocks/flip-card/block.json new file mode 100644 index 0000000..fff7e88 --- /dev/null +++ b/src/blocks/flip-card/block.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/flip-card", + "version": "1.0.0", + "title": "Flip Card", + "category": "airo-wp", + "description": "Interactive card that flips to reveal content on the back. Perfect for team profiles, product showcases, and feature highlights.", + "keywords": [ + "flip", + "card", + "interactive", + "hover", + "3d", + "rotate" + ], + "textdomain": "airo-wp", + "icon": "index-card", + "supports": { + "anchor": true, + "align": false, + "html": false, + "inserter": true, + "spacing": { + "margin": true, + "padding": false, + "__experimentalDefaultControls": { + "margin": true + } + } + }, + "attributes": { + "flipTrigger": { + "type": "string", + "default": "hover", + "enum": [ + "hover", + "click" + ] + }, + "flipEffect": { + "type": "string", + "default": "flip", + "enum": [ + "flip", + "fade", + "slide", + "zoom" + ] + }, + "flipDirection": { + "type": "string", + "default": "horizontal", + "enum": [ + "horizontal", + "vertical" + ] + }, + "flipDuration": { + "type": "string", + "default": "0.6s" + } + }, + "example": { + "attributes": { + "flipTrigger": "hover", + "flipDirection": "horizontal" + }, + "innerBlocks": [ + { + "name": "airo-wp/flip-card-face", + "attributes": { + "side": "front", + "style": { + "spacing": { + "padding": { + "top": "3rem", + "right": "2rem", + "bottom": "3rem", + "left": "2rem" + }, + "blockGap": "1rem" + }, + "color": { + "gradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + "text": "#ffffff" + } + } + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "content": "✨ Discover More", + "level": 2, + "textAlign": "center" + } + }, + { + "name": "core/paragraph", + "attributes": { + "content": "Hover to reveal amazing content", + "align": "center" + } + } + ] + }, + { + "name": "airo-wp/flip-card-face", + "attributes": { + "side": "back", + "style": { + "spacing": { + "padding": { + "top": "3rem", + "right": "2rem", + "bottom": "3rem", + "left": "2rem" + }, + "blockGap": "1.5rem" + }, + "color": { + "gradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + "text": "#ffffff" + } + } + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "content": "🚀 Ready to Start?", + "level": 2, + "textAlign": "center" + } + }, + { + "name": "core/paragraph", + "attributes": { + "content": "Add your own content here using any WordPress blocks.", + "align": "center" + } + } + ] + } + ] + }, + "viewScript": "file:./view.js", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/src/blocks/flip-card/components/FlipCardPlaceholder.js b/src/blocks/flip-card/components/FlipCardPlaceholder.js new file mode 100644 index 0000000..f84d3f1 --- /dev/null +++ b/src/blocks/flip-card/components/FlipCardPlaceholder.js @@ -0,0 +1,28 @@ +/** + * Flip Card Placeholder + * + * Thin wrapper around the shared DsgoBlockPlaceholder primitive that supplies + * the flip-card-specific copy and starter templates. Every template seeds + * both faces so the card is never single-sided after selection. + */ + +import { __ } from '@wordpress/i18n'; +import DsgoBlockPlaceholder from '../../../components/shared/DsgoBlockPlaceholder'; +import flipCardTemplates from '../templates'; + +export default function FlipCardPlaceholder({ clientId, setAttributes }) { + return ( + + ); +} diff --git a/src/blocks/flip-card/deprecated.js b/src/blocks/flip-card/deprecated.js new file mode 100644 index 0000000..7534177 --- /dev/null +++ b/src/blocks/flip-card/deprecated.js @@ -0,0 +1,95 @@ +/** + * Flip Card Block - Deprecated Versions + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Inline `width: 100%` — the version before that constant moved to style.scss. + * + * The root serialized `width:100%` into every saved card. It never varied by + * attribute and style.scss already declared it on `.airo-wp-flip-card`, so the + * inline copy was pure duplication that no Style Kit could override. Only the + * author-controlled `--airo-wp-flip-duration` is still written inline. + * + * Markup-only change: same attributes, same classes — so migrate() is a + * passthrough. + * + * NOTE: WordPress calls `isEligible(attributes, innerBlocks, { blockNode, block })` + * — there is no `innerHTML` key on that third argument. It only matters for a + * block that is otherwise VALID; for an invalid one WordPress skips isEligible + * and picks the deprecation whose save() reproduces the stored HTML. + */ +const v1 = { + // Must mirror block.json exactly — a deprecation whose `supports` omits a + // group makes WordPress strip those attributes before migrate() runs. + attributes: { + flipTrigger: { + type: 'string', + default: 'hover', + enum: ['hover', 'click'], + }, + flipEffect: { + type: 'string', + default: 'flip', + enum: ['flip', 'fade', 'slide', 'zoom'], + }, + flipDirection: { + type: 'string', + default: 'horizontal', + enum: ['horizontal', 'vertical'], + }, + flipDuration: { type: 'string', default: '0.6s' }, + }, + supports: { + anchor: true, + align: false, + html: false, + inserter: true, + spacing: { + margin: true, + padding: false, + __experimentalDefaultControls: { margin: true }, + }, + }, + isEligible(attributes, innerBlocks, extra) { + const html = getDeprecatedBlockHTML(extra); + return html.includes('airo-wp-flip-card') && html.includes('width:100%'); + }, + migrate(attributes) { + // Markup-only change. + return attributes; + }, + save({ attributes }) { + const { flipTrigger, flipEffect, flipDirection, flipDuration } = + attributes; + + const blockProps = useBlockProps.save({ + className: `airo-wp-flip-card airo-wp-flip-card--${flipTrigger} airo-wp-flip-card--effect-${flipEffect} airo-wp-flip-card--${flipDirection}`, + style: { + '--airo-wp-flip-duration': flipDuration, + width: '100%', + }, + 'data-flip-trigger': flipTrigger, + 'data-flip-effect': flipEffect, + 'data-flip-direction': flipDirection, + }); + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-flip-card__container', + }); + + return ( +
    +
    +
    + ); + }, +}; + +export { v1 }; + +export default [v1]; diff --git a/src/blocks/flip-card/edit.js b/src/blocks/flip-card/edit.js new file mode 100644 index 0000000..37fb15b --- /dev/null +++ b/src/blocks/flip-card/edit.js @@ -0,0 +1,289 @@ +/** + * Flip Card Block - Edit Component + * + * Interactive card that flips to reveal content on the back. + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { + SelectControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useSelect } from '@wordpress/data'; +import FlipCardPlaceholder from './components/FlipCardPlaceholder'; + +/** + * Flip Card Edit Component + * + * Uses a template with two airo-wp/flip-card-face children (one per + * side). Each face can contain any blocks the author wants to add. + * + * @param {Object} props Component props + * @param {Object} props.attributes Block attributes + * @param {Function} props.setAttributes Function to update attributes + * @param {string} props.clientId Block client ID + * @return {JSX.Element} Edit component + */ +export default function FlipCardEdit({ attributes, setAttributes, clientId }) { + const { flipTrigger, flipEffect, flipDirection, flipDuration } = attributes; + + // Cap child faces at two (one front + one back). airo-wp/flip-card-face + // is the canonical child block; flip-card-front / flip-card-back are + // legacy siblings kept for content already in the wild — they count + // toward the same two-face budget. `hasInnerBlocks` gates the first- + // insert placeholder (Theme 1). + const { allowedBlocks, hasInnerBlocks } = useSelect( + (select) => { + const { getBlock } = select(blockEditorStore); + const block = getBlock(clientId); + const children = block?.innerBlocks || []; + const legacyCount = children.filter( + (child) => + child.name === 'airo-wp/flip-card-front' || + child.name === 'airo-wp/flip-card-back' + ).length; + const faceCount = children.filter( + (child) => child.name === 'airo-wp/flip-card-face' + ).length; + // Empty array hides the inserter entirely — flip card is at capacity. + const allowed = + legacyCount + faceCount >= 2 + ? [] + : ['airo-wp/flip-card-face']; + return { + allowedBlocks: allowed, + hasInnerBlocks: children.length > 0, + }; + }, + [clientId] + ); + + // Block wrapper props + const blockProps = useBlockProps({ + className: 'airo-wp-flip-card', + style: { + '--airo-wp-flip-duration': flipDuration, + width: '100%', + }, + }); + + // Inner blocks configuration. Initial seeding is handled by + // FlipCardPlaceholder so authors pick a starter layout instead of + // landing on two empty faces. The `template` here is a safety net for + // "Attempt Recovery" on validation errors — it fires only when there + // are no children, which normally is caught by hasInnerBlocks above. + // templateLock is false so authors can delete a face and re-add it. + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-flip-card__container', + }, + { + allowedBlocks, + template: [ + ['airo-wp/flip-card-face', { side: 'front' }], + ['airo-wp/flip-card-face', { side: 'back' }], + ], + templateLock: false, + orientation: 'vertical', + } + ); + + if (!hasInnerBlocks) { + return ( +
    + +
    + ); + } + + return ( + <> + + + setAttributes({ + flipTrigger: 'hover', + flipEffect: 'flip', + flipDirection: 'horizontal', + flipDuration: '0.6s', + }) + } + > + flipTrigger !== 'hover'} + onDeselect={() => + setAttributes({ flipTrigger: 'hover' }) + } + isShownByDefault + > + + setAttributes({ flipTrigger: value }) + } + help={ + flipTrigger === 'hover' + ? __( + 'Card flips when hovering over it', + 'airo-wp' + ) + : __( + 'Card flips when clicking on it', + 'airo-wp' + ) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + flipEffect !== 'flip'} + onDeselect={() => setAttributes({ flipEffect: 'flip' })} + isShownByDefault + > + + setAttributes({ flipEffect: value }) + } + help={__( + 'Choose the transition animation style', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {flipEffect === 'flip' && ( + flipDirection !== 'horizontal'} + onDeselect={() => + setAttributes({ flipDirection: 'horizontal' }) + } + isShownByDefault + > + + setAttributes({ flipDirection: value }) + } + help={ + flipDirection === 'horizontal' + ? __( + 'Card flips left to right', + 'airo-wp' + ) + : __( + 'Card flips top to bottom', + 'airo-wp' + ) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + flipDuration !== '0.6s'} + onDeselect={() => + setAttributes({ flipDuration: '0.6s' }) + } + isShownByDefault + > + + setAttributes({ flipDuration: value || '0.6s' }) + } + units={[ + { value: 's', label: 's', default: 0.6 }, + { value: 'ms', label: 'ms', default: 600 }, + ]} + min={0.1} + max={3} + step={0.1} + help={__( + 'Speed of the flip animation', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    +
    +
    + + ); +} diff --git a/src/blocks/flip-card/editor.scss b/src/blocks/flip-card/editor.scss new file mode 100644 index 0000000..2a3261b --- /dev/null +++ b/src/blocks/flip-card/editor.scss @@ -0,0 +1,111 @@ +/** + * Flip Card Block - Editor Styles + * + * CRITICAL: Must be duplicated from style.scss for editor/frontend parity + * JavaScript-based flip animation (much more reliable than CSS 3D transforms) + * + * @since 1.0.0 + */ + +.airo-wp-flip-card { + position: relative; + width: 100%; + + &__container { + position: relative; + width: 100%; + } + + // Click trigger + &--click { + cursor: pointer; + + &:focus { + outline: 2px solid var(--wp--preset--color--primary, #3b82f6); + outline-offset: 2px; + } + } + + // Hover trigger + &--hover { + + &:focus { + outline: 2px solid var(--wp--preset--color--primary, #3b82f6); + outline-offset: 2px; + } + } +} + +// Editor-specific: Show both cards for editing +.editor-styles-wrapper, +.block-editor-block-preview__content { + + .airo-wp-flip-card { + // Ensure flip card respects parent width constraints + width: 100%; + + &__container { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + } + + // Both cards visible for editing. Match both the legacy + // flip-card-front/back block wrappers and the new flip-card-face + // face classes (airo-wp-flip-card__front / __back) so consolidated + // content authored via airo-wp/flip-card-face is editable. + &__container > .wp-block-airo-wp-flip-card-front, + &__container > .wp-block-airo-wp-flip-card-back, + &__container > .airo-wp-flip-card__front, + &__container > .airo-wp-flip-card__back { + box-sizing: border-box; + position: relative; + // CRITICAL: Override frontend opacity to show both cards in editor + opacity: 1 !important; + transform: none !important; + pointer-events: auto !important; + z-index: auto !important; + + // Add label in editor + &::before { + position: absolute; + top: 0.5rem; + left: 0.5rem; + font-size: 0.75rem; + font-weight: 600; + padding: 0.25rem 0.5rem; + background: rgba(0, 0, 0, 0.7); + color: #fff; + border-radius: 3px; + z-index: 100; + pointer-events: none; + } + } + + &__container > .wp-block-airo-wp-flip-card-front, + &__container > .airo-wp-flip-card__front { + + &::before { + content: 'Front'; + } + } + + &__container > .wp-block-airo-wp-flip-card-back, + &__container > .airo-wp-flip-card__back { + + &::before { + content: 'Back'; + } + } + } +} + +// Accessibility: Reduce motion preference +@media (prefers-reduced-motion: reduce) { + + .airo-wp-flip-card__container > * { + transition: opacity 0.1s ease; + transform: none !important; + } +} diff --git a/src/blocks/flip-card/index.js b/src/blocks/flip-card/index.js new file mode 100644 index 0000000..c5e72f3 --- /dev/null +++ b/src/blocks/flip-card/index.js @@ -0,0 +1,79 @@ +/** + * Flip Card Block Registration + * + * Interactive card that flips to reveal content on the back. + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; + +import edit from './edit'; +import save from './save'; +import deprecated from './deprecated'; +import metadata from './block.json'; +import { ICON_COLOR } from '../shared/constants'; + +import './editor.scss'; +import './style.scss'; + +/** + * Register Flip Card Block + */ +registerBlockType(metadata.name, { + ...metadata, + icon: { + src: ( + + {/* Card front */} + + {/* Card back (offset) */} + + {/* Rotation arrow */} + + + + ), + foreground: ICON_COLOR, + }, + edit, + save, + deprecated, +}); diff --git a/src/blocks/flip-card/save.js b/src/blocks/flip-card/save.js new file mode 100644 index 0000000..61b1a46 --- /dev/null +++ b/src/blocks/flip-card/save.js @@ -0,0 +1,45 @@ +/** + * Flip Card Block - Save Component + * + * Saves the flip card with front and back content. + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; + +/** + * Flip Card Save Component + * + * @param {Object} props Component props + * @param {Object} props.attributes Block attributes + * @return {JSX.Element} Save component + */ +export default function FlipCardSave({ attributes }) { + const { flipTrigger, flipEffect, flipDirection, flipDuration } = attributes; + + // Block wrapper props + const blockProps = useBlockProps.save({ + className: `airo-wp-flip-card airo-wp-flip-card--${flipTrigger} airo-wp-flip-card--effect-${flipEffect} airo-wp-flip-card--${flipDirection}`, + // `width: 100%` is NOT serialized — it is constant for every card and + // style.scss already declares it on `.airo-wp-flip-card`. Only the + // author-controlled flip duration is written inline. + style: { + '--airo-wp-flip-duration': flipDuration, + }, + 'data-flip-trigger': flipTrigger, + 'data-flip-effect': flipEffect, + 'data-flip-direction': flipDirection, + }); + + // Inner blocks props + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-flip-card__container', + }); + + return ( +
    +
    +
    + ); +} diff --git a/src/blocks/flip-card/style.scss b/src/blocks/flip-card/style.scss new file mode 100644 index 0000000..3b9c99a --- /dev/null +++ b/src/blocks/flip-card/style.scss @@ -0,0 +1,250 @@ +/** + * Flip Card Block - Frontend Styles + * + * JavaScript-based flip animation (much more reliable than CSS 3D transforms) + * + * @since 1.0.0 + */ + +.airo-wp-flip-card { + position: relative; + width: 100%; + height: 100%; // Fill parent grid cell when align-items: stretch + + &__container { + position: relative; + width: 100%; + height: 100%; // Propagate height from parent for equal sizing in grids + // CRITICAL: Use CSS Grid to overlay both cards while both contribute to height + display: grid; + grid-template-columns: 1fr; + // Both cards in same grid cell, grid auto-sizes to tallest + grid-template-rows: minmax(0, 1fr); + } + + // Both front and back - ensure proper box sizing + &__container > * { + width: 100%; + height: 100%; // Fill container so both cards match height + box-sizing: border-box; + transition: opacity var(--airo-wp-flip-duration, 0.6s) ease, + transform var(--airo-wp-flip-duration, 0.6s) ease; + // CRITICAL: Both occupy the same grid cell + grid-column: 1; + grid-row: 1; + // Allow content to overflow grid for height calculation + align-self: start; + } + + // Front face (visible by default) - use exact class names + &__container > .wp-block-airo-wp-flip-card-front, + &__container > .airo-wp-flip-card__front { + opacity: 1; + transform: rotateY(0deg); + z-index: 2; + } + + // Back face (hidden by default) - use exact class names + &__container > .wp-block-airo-wp-flip-card-back, + &__container > .airo-wp-flip-card__back { + opacity: 0; + transform: rotateY(180deg); + z-index: 1; + pointer-events: none; + } + + // When flipped (JS adds this class) + &.is-flipped { + // Hide front + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + opacity: 0; + transform: rotateY(-180deg); + pointer-events: none; + } + + // Show back + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + opacity: 1; + transform: rotateY(0deg); + z-index: 2; + pointer-events: auto; + } + } + + // Vertical flip direction (rotateX instead of rotateY) + &--vertical { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: rotateX(0deg); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: rotateX(180deg); + } + + &.is-flipped { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: rotateX(-180deg); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: rotateX(0deg); + } + } + } + + // Click trigger + &--click { + cursor: pointer; + + &:focus { + outline: none; + } + } + + // Hover trigger + &--hover { + + &:focus { + outline: none; + } + } + + // ===== FADE EFFECT ===== + &--effect-fade { + + .airo-wp-flip-card__container > * { + transform: none !important; + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: none !important; + } + + &.is-flipped { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: none !important; + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: none !important; + } + } + } + + // ===== SLIDE EFFECT ===== + &--effect-slide { + + .airo-wp-flip-card__container { + overflow: hidden; + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: translateX(0); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: translateX(100%); + } + + &.is-flipped { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: translateX(-100%); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: translateX(0); + } + } + + // Vertical slide for vertical direction + &.airo-wp-flip-card--vertical { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: translateY(0); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: translateY(100%); + } + + &.is-flipped { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: translateY(-100%); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: translateY(0); + } + } + } + } + + // ===== ZOOM EFFECT ===== + &--effect-zoom { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: scale(1); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: scale(0.8); + } + + &.is-flipped { + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-front, + .airo-wp-flip-card__container > .airo-wp-flip-card__front { + transform: scale(1.2); + } + + .airo-wp-flip-card__container > .wp-block-airo-wp-flip-card-back, + .airo-wp-flip-card__container > .airo-wp-flip-card__back { + transform: scale(1); + } + } + } +} + +// Accessibility: Reduce motion preference +@media (prefers-reduced-motion: reduce) { + + .airo-wp-flip-card__container > * { + transition: opacity 0.1s ease; + transform: none !important; + } + + .airo-wp-flip-card.is-flipped { + + .airo-wp-flip-card__container > [class*="flip-card-front"] { + transform: none; + } + + .airo-wp-flip-card__container > [class*="flip-card-back"] { + transform: none; + } + } +} diff --git a/src/blocks/flip-card/templates.js b/src/blocks/flip-card/templates.js new file mode 100644 index 0000000..4bd80f5 --- /dev/null +++ b/src/blocks/flip-card/templates.js @@ -0,0 +1,195 @@ +/** + * Flip Card Templates + * + * Starter layouts shown by FlipCardPlaceholder when the block is first + * inserted. Every template seeds one front face and one back face so the + * card is immediately interactive — never a single-faced state. + * + * Children are `airo-wp/flip-card-face` (the consolidated block + * introduced in Theme 2) distinguished by the `side` attribute; the + * legacy flip-card-front / flip-card-back siblings remain registered + * with `inserter: false` for existing content but are not seeded here. + */ + +import { __ } from '@wordpress/i18n'; + +// Face children support `spacing.padding` but default to none, so templates +// seed it explicitly — otherwise text sits flush against the card edge on +// first insert. Authors can still adjust via Style → Padding. +const facePadding = { + spacing: { + padding: { + top: '32px', + right: '32px', + bottom: '32px', + left: '32px', + }, + }, +}; + +const face = (side, extra = {}, innerBlocks = []) => [ + 'airo-wp/flip-card-face', + { + side, + style: { + ...facePadding, + ...(extra.style || {}), + }, + }, + innerBlocks, +]; + +// Theme-agnostic neutral colors for template starters. Hardcoded hex keeps +// the faces visibly "card-like" on any theme; authors override via Style. +// Palette matches the airo-wp wider system (slate-inspired neutrals). +const neutralBack = { + style: { + color: { + background: '#f1f5f9', + text: '#0f172a', + }, + }, +}; + +const contrastBack = { + style: { + color: { + background: '#0f172a', + text: '#ffffff', + }, + }, +}; + +const flipCardTemplates = [ + { + name: 'blank', + title: __('Blank', 'airo-wp'), + description: __('Empty front and back to fill in', 'airo-wp'), + icon: 'welcome-add-page', + attributes: {}, + innerBlocks: [face('front'), face('back')], + }, + { + name: 'feature', + title: __('Feature', 'airo-wp'), + description: __( + 'Title up front, supporting detail on the back', + 'airo-wp' + ), + icon: 'star-filled', + attributes: { flipTrigger: 'hover', flipEffect: 'flip' }, + innerBlocks: [ + face('front', {}, [ + [ + 'core/heading', + { + level: 3, + content: __('Feature title', 'airo-wp'), + textAlign: 'center', + }, + ], + ]), + face('back', neutralBack, [ + [ + 'core/paragraph', + { + content: __( + 'Add a short description of the feature, the value it delivers, or how it works.', + 'airo-wp' + ), + align: 'center', + }, + ], + ]), + ], + }, + { + name: 'profile', + title: __('Profile', 'airo-wp'), + description: __('Headshot up front, bio on the back', 'airo-wp'), + icon: 'admin-users', + attributes: { flipTrigger: 'click', flipEffect: 'flip' }, + innerBlocks: [ + face('front', {}, [ + ['core/image', { sizeSlug: 'medium' }], + [ + 'core/heading', + { + level: 4, + content: __('Name', 'airo-wp'), + textAlign: 'center', + }, + ], + [ + 'core/paragraph', + { + content: __('Role / Title', 'airo-wp'), + align: 'center', + }, + ], + ]), + face('back', {}, [ + [ + 'core/paragraph', + { + content: __( + 'Short bio. Mention background, current focus, and how to get in touch.', + 'airo-wp' + ), + align: 'center', + }, + ], + ]), + ], + }, + { + name: 'cta', + title: __('Call to Action', 'airo-wp'), + description: __( + 'Lead with a hook, finish with a button', + 'airo-wp' + ), + icon: 'megaphone', + attributes: { flipTrigger: 'hover', flipEffect: 'flip' }, + innerBlocks: [ + face('front', {}, [ + [ + 'core/heading', + { + level: 3, + content: __('Try it free', 'airo-wp'), + textAlign: 'center', + }, + ], + [ + 'core/paragraph', + { + content: __('Hover to learn more.', 'airo-wp'), + align: 'center', + }, + ], + ]), + face('back', contrastBack, [ + [ + 'core/paragraph', + { + content: __( + 'No credit card required. Cancel anytime.', + 'airo-wp' + ), + align: 'center', + }, + ], + [ + 'airo-wp/icon-button', + { + text: __('Get started', 'airo-wp'), + justification: 'center', + }, + ], + ]), + ], + }, +]; + +export default flipCardTemplates; diff --git a/src/blocks/flip-card/view.js b/src/blocks/flip-card/view.js new file mode 100644 index 0000000..76a4e72 --- /dev/null +++ b/src/blocks/flip-card/view.js @@ -0,0 +1,86 @@ +/** + * Flip Card Frontend JavaScript + * + * Handles flip interactions on the frontend based on trigger type. + * + * @since 1.0.0 + */ + +function initFlipCards() { + const flipCards = document.querySelectorAll('.airo-wp-flip-card'); + + flipCards.forEach((card) => { + // Prevent duplicate initialization + if (card.dataset.dsgoInitialized) { + return; + } + card.dataset.dsgoInitialized = 'true'; + + const flipTrigger = card.getAttribute('data-flip-trigger'); + + if (flipTrigger === 'click') { + // Click trigger: toggle flip state + card.addEventListener('click', function (e) { + // Don't trigger if clicking on a link or button inside the card + const isInteractive = + e.target.tagName === 'A' || + e.target.tagName === 'BUTTON' || + e.target.closest('a') || + e.target.closest('button'); + + if (!isInteractive) { + card.classList.toggle('is-flipped'); + } + }); + + // Add keyboard support for accessibility + card.setAttribute('tabindex', '0'); + card.setAttribute('role', 'button'); + card.setAttribute('aria-label', 'Flip card to reveal content'); + + card.addEventListener('keydown', function (e) { + // Space or Enter key + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + card.classList.toggle('is-flipped'); + + // Update aria-label based on state + const isFlipped = card.classList.contains('is-flipped'); + card.setAttribute( + 'aria-label', + isFlipped + ? 'Flip card back to front' + : 'Flip card to reveal content' + ); + } + }); + } else if (flipTrigger === 'hover') { + // Hover trigger: add/remove flip class on hover + card.addEventListener('mouseenter', function () { + card.classList.add('is-flipped'); + }); + + card.addEventListener('mouseleave', function () { + card.classList.remove('is-flipped'); + }); + + // Keyboard support for hover cards + card.setAttribute('tabindex', '0'); + card.setAttribute( + 'aria-label', + 'Flip card - hover or focus to reveal content' + ); + + card.addEventListener('focus', function () { + card.classList.add('is-flipped'); + }); + + card.addEventListener('blur', function () { + card.classList.remove('is-flipped'); + }); + } + }); +} + +document.addEventListener('DOMContentLoaded', initFlipCards); +document.addEventListener('airo-wp-content-loaded', initFlipCards); diff --git a/src/blocks/form-builder/block.json b/src/blocks/form-builder/block.json new file mode 100644 index 0000000..b5eb20b --- /dev/null +++ b/src/blocks/form-builder/block.json @@ -0,0 +1,240 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-builder", + "version": "1.0.0", + "title": "Form Builder", + "category": "airo-wp", + "description": "Create custom forms with multiple field types, AJAX submission, and spam protection.", + "keywords": [ + "form", + "contact", + "submit", + "fields", + "survey" + ], + "textdomain": "airo-wp", + "icon": "forms", + "attributes": { + "formId": { + "type": "string", + "default": "" + }, + "hasFields": { + "type": "boolean", + "default": true + }, + "submitButtonText": { + "type": "string", + "default": "Submit" + }, + "submitButtonAlignment": { + "type": "string", + "default": "left", + "enum": [ + "left", + "center", + "right" + ] + }, + "submitButtonPosition": { + "type": "string", + "default": "below", + "enum": [ + "below", + "inline" + ] + }, + "ajaxSubmit": { + "type": "boolean", + "default": true + }, + "successMessage": { + "type": "string", + "default": "Thank you! Your form has been submitted successfully." + }, + "errorMessage": { + "type": "string", + "default": "There was an error submitting the form. Please try again." + }, + "redirectUrl": { + "type": "string", + "default": "" + }, + "fieldSpacing": { + "type": "string", + "default": "" + }, + "inputHeight": { + "type": "string", + "default": "" + }, + "inputPadding": { + "type": "string", + "default": "" + }, + "fieldLabelColor": { + "type": "string", + "default": "" + }, + "fieldBorderColor": { + "type": "string", + "default": "" + }, + "fieldBackgroundColor": { + "type": "string", + "default": "" + }, + "fieldBorderRadius": { + "type": "string", + "default": "" + }, + "submitButtonColor": { + "type": "string", + "default": "" + }, + "submitButtonBackgroundColor": { + "type": "string", + "default": "" + }, + "submitButtonPaddingVertical": { + "type": "string", + "default": "" + }, + "submitButtonPaddingHorizontal": { + "type": "string", + "default": "" + }, + "submitButtonFontSize": { + "type": "string", + "default": "" + }, + "submitButtonHeight": { + "type": "string", + "default": "" + }, + "submitButtonHoverColor": { + "type": "string", + "default": "" + }, + "submitButtonHoverBackgroundColor": { + "type": "string", + "default": "" + }, + "enableHoneypot": { + "type": "boolean", + "default": true + }, + "enableRateLimit": { + "type": "boolean", + "default": true + }, + "rateLimitCount": { + "type": "number", + "default": 3 + }, + "rateLimitWindow": { + "type": "number", + "default": 60 + }, + "enableTurnstile": { + "type": "boolean", + "default": false + }, + "enableEmail": { + "type": "boolean", + "default": true + }, + "emailTo": { + "type": "string", + "default": "" + }, + "emailSubject": { + "type": "string", + "default": "New Form Submission" + }, + "emailFromName": { + "type": "string", + "default": "" + }, + "emailFromEmail": { + "type": "string", + "default": "" + }, + "emailReplyTo": { + "type": "string", + "default": "" + }, + "emailBody": { + "type": "string", + "default": "" + } + }, + "providesContext": { + "airo-wp/form/submitButtonText": "submitButtonText", + "airo-wp/form/submitButtonColor": "submitButtonColor", + "airo-wp/form/fieldLabelColor": "fieldLabelColor", + "airo-wp/form/fieldBorderColor": "fieldBorderColor", + "airo-wp/form/fieldBackgroundColor": "fieldBackgroundColor", + "airo-wp/form/fieldSpacing": "fieldSpacing", + "airo-wp/form/fieldBorderRadius": "fieldBorderRadius", + "airo-wp/form/inputHeight": "inputHeight", + "airo-wp/form/inputPadding": "inputPadding", + "airo-wp/form/formId": "formId" + }, + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": true + } + }, + "color": { + "background": true, + "text": true, + "__experimentalDefaultControls": { + "background": false, + "text": false + } + } + }, + "example": { + "attributes": { + "submitButtonText": "Send Message" + }, + "innerBlocks": [ + { + "name": "airo-wp/form-text-field", + "attributes": { + "label": "Name", + "required": true + } + }, + { + "name": "airo-wp/form-email-field", + "attributes": { + "label": "Email", + "required": true + } + }, + { + "name": "airo-wp/form-textarea-field", + "attributes": { + "label": "Message" + } + } + ] + }, + "editorScript": "file:./index.js", + "viewScript": "file:./view.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css" +} diff --git a/src/blocks/form-builder/components/FormBuilderPlaceholder.js b/src/blocks/form-builder/components/FormBuilderPlaceholder.js new file mode 100644 index 0000000..0f1f802 --- /dev/null +++ b/src/blocks/form-builder/components/FormBuilderPlaceholder.js @@ -0,0 +1,28 @@ +/** + * Form Builder Placeholder + * + * Thin wrapper around the shared DsgoBlockPlaceholder primitive. The "Blank" + * template seeds a single field so authors who skip the chooser still land in a + * working form rather than an empty container. + */ + +import { __ } from '@wordpress/i18n'; +import DsgoBlockPlaceholder from '../../../components/shared/DsgoBlockPlaceholder'; +import { formBuilderTemplates } from '../templates'; + +export default function FormBuilderPlaceholder({ clientId, setAttributes }) { + return ( + + ); +} diff --git a/src/blocks/form-builder/deprecated.js b/src/blocks/form-builder/deprecated.js new file mode 100644 index 0000000..011c958 --- /dev/null +++ b/src/blocks/form-builder/deprecated.js @@ -0,0 +1,1076 @@ +/** + * Form Builder Block - Deprecations + * + * Handles migration from older save formats. + * + * @since 2.3.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { + convertColorToCSSVar, + convertPresetToCSSVar, +} from '../../utils/convert-preset-to-css-var'; +import { validateCSSLength } from '../../utils/css-generator'; +import metadata from './block.json'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * V4 deprecation: Before the border-color fallback moved from inline style to CSS. + * + * Previously save() always forced a `--airo-wp-form-border-color` custom property + * onto the wrapper — falling back to the literal `#d1d5db` whenever + * fieldBorderColor was empty. That made it impossible for hand-authored markup + * (patterns) to omit the property and simply inherit the block's CSS default. + * The fix drops the inline fallback; `.airo-wp-form-builder` in style.scss now + * supplies `#d1d5db` as the CSS default instead. + */ +// Attribute schema frozen at the spacing / sizing defaults that were baked into +// the wrapper + submit button UNCONDITIONALLY before those tokens became +// nullable and theme-inheritable. Every deprecation below parses implicit +// (unset) forms with these, so its save() reproduces the values actually +// present in the stored HTML — even though the live block.json now defaults them +// to '' (inherit). +const legacyAttributes = { + ...metadata.attributes, + fieldSpacing: { type: 'string', default: '1.5rem' }, + inputHeight: { type: 'string', default: '44px' }, + inputPadding: { type: 'string', default: '0.75rem' }, + submitButtonHeight: { type: 'string', default: '44px' }, + submitButtonPaddingVertical: { type: 'string', default: '0.75rem' }, + submitButtonPaddingHorizontal: { type: 'string', default: '2rem' }, +}; + +/** + * V5 deprecation: before the spacing / sizing tokens became nullable + * (removable) and the submit button inherited the theme's global button styles. + * + * The old save() wrote --airo-wp-form-field-spacing / --airo-wp-form-input-height / + * --airo-wp-form-input-padding on the wrapper and min-height / padding on the + * button for every form, even at their defaults. The current save() omits them + * at their defaults so the form inherits the theme. isEligible matches any form + * that baked the input-height token (all pre-change forms did); migrate strips + * values that equal the old defaults so they inherit, keeping real overrides. + */ +const v5 = { + attributes: legacyAttributes, + supports: metadata.supports, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // The var alone does NOT mean "legacy": the current save() emits it too, + // as soon as inputHeight is set. The old save() wrote it UNCONDITIONALLY, + // with a baked default, which is why legacy content carries it while the + // block comment has no inputHeight. Current content that renders the var + // always carries the attribute (non-default, so WordPress serializes it), + // so requiring its absence excludes current content only. + return ( + Boolean(innerHTML) && + !attributes.inputHeight && + innerHTML.includes('--airo-wp-form-input-height') + ); + }, + migrate(attributes) { + const strip = (value, def) => (value === def ? '' : value); + return { + ...attributes, + fieldSpacing: strip(attributes.fieldSpacing, '1.5rem'), + inputHeight: strip(attributes.inputHeight, '44px'), + inputPadding: strip(attributes.inputPadding, '0.75rem'), + submitButtonHeight: strip(attributes.submitButtonHeight, '44px'), + submitButtonPaddingVertical: strip( + attributes.submitButtonPaddingVertical, + '0.75rem' + ), + submitButtonPaddingHorizontal: strip( + attributes.submitButtonPaddingHorizontal, + '2rem' + ), + }; + }, + save({ attributes }) { + const { + formId, + hasFields, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + fieldBorderRadius, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + submitButtonHoverColor, + submitButtonHoverBackgroundColor, + enableHoneypot, + enableTurnstile, + redirectUrl, + } = attributes; + + if (!hasFields) { + return null; + } + + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': + submitButtonPosition === 'inline', + }); + + const formStyles = { + '--airo-wp-form-field-spacing': fieldSpacing, + '--airo-wp-form-input-height': inputHeight, + '--airo-wp-form-input-padding': inputPadding, + '--airo-wp-form-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-form-border-color': convertColorToCSSVar(fieldBorderColor), + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + '--airo-wp-form-border-radius': validateCSSLength(fieldBorderRadius), + }; + + const submitButtonStyle = { + ...(submitButtonColor && { + color: convertColorToCSSVar(submitButtonColor), + }), + ...(submitButtonBackgroundColor && { + backgroundColor: convertColorToCSSVar( + submitButtonBackgroundColor + ), + }), + minHeight: submitButtonHeight, + paddingTop: submitButtonPaddingVertical, + paddingBottom: submitButtonPaddingVertical, + paddingLeft: submitButtonPaddingHorizontal, + paddingRight: submitButtonPaddingHorizontal, + ...(submitButtonFontSize && { fontSize: submitButtonFontSize }), + ...(submitButtonHoverBackgroundColor && { + '--airo-wp-button-hover-bg': convertColorToCSSVar( + submitButtonHoverBackgroundColor + ), + }), + ...(submitButtonHoverColor && { + '--airo-wp-button-hover-color': convertColorToCSSVar( + submitButtonHoverColor + ), + }), + }; + + const blockProps = useBlockProps.save({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + 'data-ajax-submit': ajaxSubmit, + 'data-success-message': successMessage, + 'data-error-message': errorMessage, + 'data-submit-text': submitButtonText, + ...(enableTurnstile && { + 'data-airo-wp-turnstile': 'true', + }), + ...(redirectUrl && { + 'data-redirect-url': redirectUrl, + }), + }); + + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps.save({ + className: 'airo-wp-form__fields', + }); + + return ( +
    +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {enableHoneypot && ( + + )} + + + + {enableTurnstile && ( +
    + )} + + {submitButtonPosition === 'below' && ( +
    + +
    + )} + +
    + +
    + ); + }, +}; + +const v4 = { + attributes: legacyAttributes, + supports: metadata.supports, + // No isEligible: markup-change deprecation, reached by save-matching on an + // INVALID block (WordPress skips isEligible for those). The old guard, + // `!attributes.fieldBorderColor`, was true of every CURRENT form that simply + // never customised the border colour, so it claimed current content. Whether + // the old forced-fallback markup is actually present is decided by matching + // this version's save() against the stored HTML, which is the real test. + migrate(attributes) { + return attributes; + }, + save({ attributes }) { + const { + formId, + hasFields, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + fieldBorderRadius, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + submitButtonHoverColor, + submitButtonHoverBackgroundColor, + enableHoneypot, + enableTurnstile, + redirectUrl, + } = attributes; + + if (!hasFields) { + return null; + } + + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': + submitButtonPosition === 'inline', + }); + + const formStyles = { + '--airo-wp-form-field-spacing': fieldSpacing, + '--airo-wp-form-input-height': inputHeight, + '--airo-wp-form-input-padding': inputPadding, + '--airo-wp-form-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-form-border-color': + convertColorToCSSVar(fieldBorderColor) || '#d1d5db', + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + '--airo-wp-form-border-radius': validateCSSLength(fieldBorderRadius), + }; + + const blockProps = useBlockProps.save({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + 'data-ajax-submit': ajaxSubmit, + 'data-success-message': successMessage, + 'data-error-message': errorMessage, + 'data-submit-text': submitButtonText, + ...(enableTurnstile && { + 'data-airo-wp-turnstile': 'true', + }), + ...(redirectUrl && { + 'data-redirect-url': redirectUrl, + }), + }); + + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps.save({ + className: 'airo-wp-form__fields', + }); + + return ( +
    +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {enableHoneypot && ( + + )} + + + + {enableTurnstile && ( +
    + )} + + {submitButtonPosition === 'below' && ( +
    + +
    + )} + +
    + +
    + ); + }, +}; + +/** + * V3 deprecation: Before convertColorToCSSVar was applied to form style properties. + * + * Raw empty-string attribute values were passed directly into inline CSS custom + * properties, producing `--airo-wp-form-label-color:;` and `--airo-wp-form-field-bg:` + * in the serialised markup. After the fix, convertColorToCSSVar/convertPresetToCSSVar + * returns undefined for empty strings, so React omits those properties entirely. + * + * This version also predates: fieldBorderRadius / --airo-wp-form-border-radius, + * redirectUrl / data-redirect-url, submitButtonHoverColor / + * submitButtonHoverBackgroundColor on the button, and the hasFields early-return. + */ +const v3 = { + attributes: legacyAttributes, + supports: metadata.supports, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v3 blocks have raw empty CSS vars rendered as `--airo-wp-form-label-color:;` + return innerHTML && innerHTML.includes('--airo-wp-form-label-color:;'); + }, + migrate(attributes) { + return attributes; + }, + save({ attributes }) { + const { + formId, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + enableHoneypot, + enableTurnstile, + } = attributes; + + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': + submitButtonPosition === 'inline', + }); + + // Raw values — no convertColorToCSSVar; empty strings render as empty CSS vars. + const formStyles = { + '--airo-wp-form-field-spacing': fieldSpacing, + '--airo-wp-form-input-height': inputHeight, + '--airo-wp-form-input-padding': inputPadding, + '--airo-wp-form-label-color': fieldLabelColor, + '--airo-wp-form-border-color': fieldBorderColor || '#d1d5db', + '--airo-wp-form-field-bg': fieldBackgroundColor, + }; + + const blockProps = useBlockProps.save({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + 'data-ajax-submit': ajaxSubmit, + 'data-success-message': successMessage, + 'data-error-message': errorMessage, + 'data-submit-text': submitButtonText, + ...(enableTurnstile && { + 'data-airo-wp-turnstile': 'true', + }), + }); + + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps.save({ + className: 'airo-wp-form__fields', + }); + + return ( +
    +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {enableHoneypot && ( + + )} + + + + {enableTurnstile && ( +
    + )} + + {submitButtonPosition === 'below' && ( +
    + +
    + )} + +
    + +
    + ); + }, +}; + +/** + * V2 deprecation: Before aria-hidden and aria-atomic were added. + * + * The honeypot input lacked aria-hidden="true" and the message div lacked + * aria-atomic="true". These were added for accessibility improvements but + * content generated by the site-designer-api predates these additions. + */ +const v2 = { + attributes: legacyAttributes, + supports: metadata.supports, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v2 blocks lack aria-hidden on honeypot and aria-atomic on message div + return ( + innerHTML && + ((innerHTML.includes('dsg_website') && + !innerHTML.includes('aria-hidden')) || + (innerHTML.includes('airo-wp-form__message') && + !innerHTML.includes('aria-atomic'))) + ); + }, + migrate(attributes) { + return attributes; + }, + save({ attributes }) { + const { + formId, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + submitButtonHoverColor, + submitButtonHoverBackgroundColor, + enableHoneypot, + enableTurnstile, + } = attributes; + + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': + submitButtonPosition === 'inline', + }); + + const formStyles = { + '--airo-wp-form-field-spacing': fieldSpacing, + '--airo-wp-form-input-height': inputHeight, + '--airo-wp-form-input-padding': inputPadding, + '--airo-wp-form-label-color': convertPresetToCSSVar(fieldLabelColor), + '--airo-wp-form-border-color': + convertPresetToCSSVar(fieldBorderColor) || '#d1d5db', + '--airo-wp-form-field-bg': convertPresetToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps.save({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + 'data-ajax-submit': ajaxSubmit, + 'data-success-message': successMessage, + 'data-error-message': errorMessage, + 'data-submit-text': submitButtonText, + ...(enableTurnstile && { + 'data-airo-wp-turnstile': 'true', + }), + }); + + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps.save({ + className: 'airo-wp-form__fields', + }); + + return ( +
    +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {enableHoneypot && ( + + )} + + + + {enableTurnstile && ( +
    + )} + + {submitButtonPosition === 'below' && ( +
    + +
    + )} + +
    + +
    + ); + }, +}; + +/** + * V1 deprecation: Email config was exposed as data attributes. + * + * Email settings (data-enable-email, data-email-to, data-email-subject, etc.) + * were rendered as HTML data attributes on the block wrapper. This was a + * deliverability and security concern because: + * - Email configuration was visible in page source + * - Client-side JavaScript sent these values in the REST API request + * - An attacker could modify the request to send email to arbitrary addresses + * + * The fix moves email configuration to server-side-only: the PHP handler looks + * up block attributes directly from post content when processing submissions. + */ +const v1 = { + attributes: legacyAttributes, + supports: metadata.supports, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v1 blocks have email config exposed as data attributes + return innerHTML && innerHTML.includes('data-enable-email'); + }, + migrate(attributes) { + return attributes; + }, + save({ attributes }) { + const { + formId, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + enableHoneypot, + enableTurnstile, + enableEmail, + emailTo, + emailSubject, + emailFromName, + emailFromEmail, + emailReplyTo, + emailBody, + } = attributes; + + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': + submitButtonPosition === 'inline', + }); + + const formStyles = { + '--airo-wp-form-field-spacing': fieldSpacing, + '--airo-wp-form-input-height': inputHeight, + '--airo-wp-form-input-padding': inputPadding, + '--airo-wp-form-label-color': convertPresetToCSSVar(fieldLabelColor), + '--airo-wp-form-border-color': + convertPresetToCSSVar(fieldBorderColor) || '#d1d5db', + '--airo-wp-form-field-bg': convertPresetToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps.save({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + 'data-ajax-submit': ajaxSubmit, + 'data-success-message': successMessage, + 'data-error-message': errorMessage, + 'data-submit-text': submitButtonText, + 'data-enable-email': enableEmail, + 'data-email-to': emailTo, + 'data-email-subject': emailSubject, + 'data-email-from-name': emailFromName, + 'data-email-from-email': emailFromEmail, + 'data-email-reply-to': emailReplyTo, + 'data-email-body': emailBody, + ...(enableTurnstile && { + 'data-airo-wp-turnstile': 'true', + }), + }); + + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps.save({ + className: 'airo-wp-form__fields', + }); + + return ( +
    +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {enableHoneypot && ( + + )} + + + + {enableTurnstile && ( +
    + )} + + {submitButtonPosition === 'below' && ( +
    + +
    + )} + +
    + +
    + ); + }, +}; + +const deprecated = [v5, v4, v3, v2, v1]; + +export default deprecated; diff --git a/src/blocks/form-builder/edit.js b/src/blocks/form-builder/edit.js new file mode 100644 index 0000000..3e40d6f --- /dev/null +++ b/src/blocks/form-builder/edit.js @@ -0,0 +1,1326 @@ +/** + * Form Builder Block - Editor Component + * + * @since 1.0.0 + */ + +import { __, sprintf } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InspectorControls, + useSettings, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { + Notice, + TextControl, + TextareaControl, + ToggleControl, + RangeControl, + SelectControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import classnames from 'classnames'; +import { useUniqueBlockId } from '../../hooks'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import { validateCSSLength } from '../../utils/css-generator'; +import FormBuilderPlaceholder from './components/FormBuilderPlaceholder'; + +// Blocks that Gutenberg identifies as form fields for the reply-to dropdown. +const EMAILABLE_FIELD_BLOCKS = new Set([ + 'airo-wp/form-text-field', + 'airo-wp/form-email-field', + 'airo-wp/form-textarea-field', + 'airo-wp/form-number-field', + 'airo-wp/form-phone-field', + 'airo-wp/form-url-field', + 'airo-wp/form-date-field', + 'airo-wp/form-time-field', + 'airo-wp/form-select-field', + 'airo-wp/form-checkbox-field', + 'airo-wp/form-hidden-field', +]); + +export default function FormBuilderEdit({ + attributes, + setAttributes, + clientId, +}) { + const { + formId, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + fieldBorderRadius, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + submitButtonHoverColor, + submitButtonHoverBackgroundColor, + enableHoneypot, + enableRateLimit, + rateLimitCount, + rateLimitWindow, + enableTurnstile, + enableEmail, + emailTo, + emailSubject, + emailFromName, + emailFromEmail, + emailReplyTo, + emailBody, + redirectUrl, + } = attributes; + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Read the site-wide default hover animation from theme.json custom settings. + const [themeDefaultHover] = useSettings( + 'custom.airowp.defaultIconButtonHover' + ); + + // Resolve the effective animation for editor preview. + // Form submit always uses the site-wide default — no per-block override. + // Validate against allowlist to match backend (class-plugin.php ALLOWED_HOVER_ANIMATIONS). + const ALLOWED_ANIMATIONS = [ + 'fill-diagonal', + 'zoom-in', + 'slide-left', + 'slide-right', + 'slide-down', + 'slide-up', + 'border-pulse', + 'border-glow', + 'lift', + 'shrink', + ]; + const adminDefault = themeDefaultHover || 'none'; + const isValidAnimation = ALLOWED_ANIMATIONS.includes(adminDefault); + const effectiveAnimation = + isValidAnimation && adminDefault !== 'none' ? adminDefault : null; + + // Build animation class for the submit button + const submitAnimationClass = + effectiveAnimation && effectiveAnimation !== 'none' + ? ` airo-wp-form__submit--${effectiveAnimation}` + : ''; + + useUniqueBlockId({ + clientId, + attributeName: 'formId', + value: formId, + setAttributes, + }); + + // Track child count so we can show the template chooser on first insert, + // and build the reply-to dropdown options from the actual form fields. + // + // Return the store's already-memoized `getBlocks` array directly rather than + // a freshly-derived object/array: useSelect compares its mapSelect result by + // reference, so building `.filter().map()` output inside the selector makes + // it unstable on every call (the "useSelect returns different values" dev + // warning). Derive the dropdown options in a useMemo keyed on that stable + // array instead. + // `getBlocks` always returns an array (stable `[]` for an unknown clientId), + // so `|| []` short-circuits to that same reference and never reintroduces the + // instability — it's kept purely as a defensive guard. + const childBlocks = useSelect( + (select) => select('core/block-editor').getBlocks(clientId) || [], + [clientId] + ); + const hasInnerBlocks = childBlocks.length > 0; + const replyToFieldOptions = useMemo( + () => + childBlocks + .filter((child) => EMAILABLE_FIELD_BLOCKS.has(child.name)) + .map((child) => ({ + name: child.attributes?.fieldName || '', + label: child.attributes?.label || '', + })) + .filter((field) => !!field.name), + [childBlocks] + ); + + // Keep hasFields in sync with the actual child field count so save.js can + // suppress the submit button/form wrapper when no template is picked. + useEffect(() => { + if (attributes.hasFields !== hasInnerBlocks) { + setAttributes({ hasFields: hasInnerBlocks }); + } + }, [hasInnerBlocks, attributes.hasFields, setAttributes]); + + // Calculate classes + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': submitButtonPosition === 'inline', + }); + + // Apply form settings as CSS custom properties - MUST MATCH save.js. + // Spacing/sizing tokens are only written when explicitly set; otherwise they + // inherit the theme.json custom properties defined in style.scss. + const formStyles = { + ...(fieldSpacing && { '--airo-wp-form-field-spacing': fieldSpacing }), + ...(inputHeight && { '--airo-wp-form-input-height': inputHeight }), + ...(inputPadding && { '--airo-wp-form-input-padding': inputPadding }), + '--airo-wp-form-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-form-border-color': convertColorToCSSVar(fieldBorderColor), + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + '--airo-wp-form-border-radius': validateCSSLength(fieldBorderRadius), + // Button colors now applied as inline styles on button element + }; + + // Submit button style - MUST MATCH save.js. Sizing is only written when + // explicitly set; otherwise the button inherits the theme's global button + // styles via wp-element-button. + const submitButtonStyle = { + ...(submitButtonColor && { + color: convertColorToCSSVar(submitButtonColor), + }), + ...(submitButtonBackgroundColor && { + backgroundColor: convertColorToCSSVar(submitButtonBackgroundColor), + }), + ...(submitButtonHeight && { minHeight: submitButtonHeight }), + ...(submitButtonPaddingVertical && { + paddingTop: submitButtonPaddingVertical, + paddingBottom: submitButtonPaddingVertical, + }), + ...(submitButtonPaddingHorizontal && { + paddingLeft: submitButtonPaddingHorizontal, + paddingRight: submitButtonPaddingHorizontal, + }), + ...(submitButtonFontSize && { fontSize: submitButtonFontSize }), + ...(submitButtonHoverBackgroundColor && { + '--airo-wp-button-hover-bg': convertColorToCSSVar( + submitButtonHoverBackgroundColor + ), + }), + ...(submitButtonHoverColor && { + '--airo-wp-button-hover-color': convertColorToCSSVar( + submitButtonHoverColor + ), + }), + }; + + const blockProps = useBlockProps({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + }); + + // Inner blocks for form fields. The first-insert template chooser seeds + // innerBlocks itself, so we omit a default template here — otherwise the + // placeholder would never show. + // Extract children so we can add button inside fields container. + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps( + { + className: 'airo-wp-form__fields', + }, + { + allowedBlocks: [ + 'airo-wp/form-text-field', + 'airo-wp/form-email-field', + 'airo-wp/form-textarea-field', + 'airo-wp/form-number-field', + 'airo-wp/form-phone-field', + 'airo-wp/form-url-field', + 'airo-wp/form-date-field', + 'airo-wp/form-time-field', + 'airo-wp/form-select-field', + 'airo-wp/form-checkbox-field', + 'airo-wp/form-hidden-field', + ], + orientation: 'vertical', + } + ); + + // Show the template chooser when the form is empty. + if (!hasInnerBlocks) { + return ( +
    + +
    + ); + } + + return ( + <> + + + setAttributes({ + submitButtonText: 'Submit', + submitButtonAlignment: 'left', + submitButtonPosition: 'below', + ajaxSubmit: true, + successMessage: + 'Thank you! Your form has been submitted successfully.', + errorMessage: + 'There was an error submitting the form. Please try again.', + redirectUrl: '', + fieldSpacing: '', + inputHeight: '', + inputPadding: '', + fieldBorderRadius: '', + submitButtonHeight: '', + submitButtonPaddingVertical: '', + submitButtonPaddingHorizontal: '', + submitButtonFontSize: '', + enableHoneypot: true, + enableRateLimit: true, + rateLimitCount: 3, + rateLimitWindow: 60, + enableTurnstile: false, + enableEmail: true, + emailTo: '', + emailSubject: 'New Form Submission', + emailFromName: '', + emailFromEmail: '', + emailReplyTo: '', + emailBody: '', + }) + } + > + ajaxSubmit !== true} + onDeselect={() => setAttributes({ ajaxSubmit: true })} + isShownByDefault + > + + setAttributes({ ajaxSubmit: value }) + } + help={__( + 'Submit form without page reload', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + submitButtonText !== 'Submit'} + onDeselect={() => + setAttributes({ submitButtonText: 'Submit' }) + } + isShownByDefault + > + + setAttributes({ submitButtonText: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + submitButtonPosition !== 'below'} + onDeselect={() => + setAttributes({ submitButtonPosition: 'below' }) + } + isShownByDefault + > + + setAttributes({ submitButtonPosition: value }) + } + help={__( + 'Place button below all fields or inline with the last field (useful for subscribe forms)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {submitButtonPosition === 'below' && ( + submitButtonAlignment !== 'left'} + onDeselect={() => + setAttributes({ submitButtonAlignment: 'left' }) + } + isShownByDefault + > + + setAttributes({ + submitButtonAlignment: value, + }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + submitButtonHeight !== ''} + onDeselect={() => + setAttributes({ submitButtonHeight: '' }) + } + isShownByDefault + > + + setAttributes({ + submitButtonHeight: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 44 }, + { value: 'rem', label: 'rem', default: 2.75 }, + { value: 'em', label: 'em', default: 2.75 }, + ]} + min={28} + max={200} + help={__( + 'Minimum height for submit button', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + submitButtonPaddingVertical !== ''} + onDeselect={() => + setAttributes({ + submitButtonPaddingVertical: '', + }) + } + isShownByDefault + > + + setAttributes({ + submitButtonPaddingVertical: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 12 }, + { value: 'rem', label: 'rem', default: 0.75 }, + { value: 'em', label: 'em', default: 0.75 }, + ]} + min={0} + max={50} + help={__( + 'Top and bottom padding for button', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + submitButtonPaddingHorizontal !== ''} + onDeselect={() => + setAttributes({ + submitButtonPaddingHorizontal: '', + }) + } + isShownByDefault + > + + setAttributes({ + submitButtonPaddingHorizontal: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 32 }, + { value: 'rem', label: 'rem', default: 2 }, + { value: 'em', label: 'em', default: 2 }, + ]} + min={0} + max={100} + help={__( + 'Left and right padding for button', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + submitButtonFontSize !== ''} + onDeselect={() => + setAttributes({ submitButtonFontSize: '' }) + } + isShownByDefault + > + + setAttributes({ + submitButtonFontSize: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 16 }, + { value: 'rem', label: 'rem', default: 1 }, + { value: 'em', label: 'em', default: 1 }, + ]} + min={10} + max={100} + help={__( + 'Font size for button text (leave empty to inherit)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldSpacing !== ''} + onDeselect={() => setAttributes({ fieldSpacing: '' })} + isShownByDefault + > + + setAttributes({ + fieldSpacing: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 24 }, + { value: 'rem', label: 'rem', default: 1.5 }, + { value: 'em', label: 'em', default: 1.5 }, + ]} + min={0} + max={100} + help={__( + 'Space between form fields', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + inputHeight !== ''} + onDeselect={() => setAttributes({ inputHeight: '' })} + isShownByDefault + > + + setAttributes({ inputHeight: value || '' }) + } + units={[ + { value: 'px', label: 'px', default: 44 }, + { value: 'rem', label: 'rem', default: 2.75 }, + { value: 'em', label: 'em', default: 2.75 }, + ]} + min={28} + max={200} + help={__( + 'Minimum height for input fields', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + inputPadding !== ''} + onDeselect={() => setAttributes({ inputPadding: '' })} + isShownByDefault + > + + setAttributes({ + inputPadding: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 12 }, + { value: 'rem', label: 'rem', default: 0.75 }, + { value: 'em', label: 'em', default: 0.75 }, + ]} + min={0} + max={50} + help={__( + 'Padding inside input fields', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldBorderRadius !== ''} + onDeselect={() => + setAttributes({ fieldBorderRadius: '' }) + } + isShownByDefault + > + + setAttributes({ + fieldBorderRadius: value || '', + }) + } + units={[ + { value: 'px', label: 'px', default: 6 }, + { value: 'rem', label: 'rem', default: 0.375 }, + { value: 'em', label: 'em', default: 0.375 }, + ]} + min={0} + max={100} + help={__( + 'Border radius for input fields', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + successMessage !== + 'Thank you! Your form has been submitted successfully.' + } + onDeselect={() => + setAttributes({ + successMessage: + 'Thank you! Your form has been submitted successfully.', + }) + } + isShownByDefault + > + + setAttributes({ successMessage: value }) + } + help={__( + 'Message shown after successful submission', + 'airo-wp' + )} + rows={3} + __nextHasNoMarginBottom + /> + + + + errorMessage !== + 'There was an error submitting the form. Please try again.' + } + onDeselect={() => + setAttributes({ + errorMessage: + 'There was an error submitting the form. Please try again.', + }) + } + isShownByDefault + > + + setAttributes({ errorMessage: value }) + } + help={__( + 'Message shown if submission fails', + 'airo-wp' + )} + rows={3} + __nextHasNoMarginBottom + /> + + + redirectUrl !== ''} + onDeselect={() => setAttributes({ redirectUrl: '' })} + isShownByDefault + > + + setAttributes({ redirectUrl: value }) + } + type="url" + placeholder="https://example.com/thank-you" + help={__( + 'Redirect to this URL after successful submission. Leave empty to show the success message instead. Requires AJAX Submit to be enabled.', + 'airo-wp' + )} + disabled={!ajaxSubmit} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + enableHoneypot !== true} + onDeselect={() => + setAttributes({ enableHoneypot: true }) + } + isShownByDefault + > + + setAttributes({ enableHoneypot: value }) + } + help={__( + 'Invisible field to catch spam bots', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + enableRateLimit !== true} + onDeselect={() => + setAttributes({ enableRateLimit: true }) + } + isShownByDefault + > + + setAttributes({ enableRateLimit: value }) + } + help={__( + 'Limit submissions per IP address', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + {enableRateLimit && ( + rateLimitCount !== 3} + onDeselect={() => + setAttributes({ rateLimitCount: 3 }) + } + isShownByDefault + > + + setAttributes({ rateLimitCount: value }) + } + min={1} + max={10} + help={__( + 'Maximum submissions allowed per time window', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {enableRateLimit && ( + rateLimitWindow !== 60} + onDeselect={() => + setAttributes({ rateLimitWindow: 60 }) + } + isShownByDefault + > + + setAttributes({ rateLimitWindow: value }) + } + min={30} + max={300} + step={30} + help={__( + 'Time period for rate limiting', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + enableTurnstile !== false} + onDeselect={() => + setAttributes({ enableTurnstile: false }) + } + isShownByDefault + > + + setAttributes({ enableTurnstile: value }) + } + help={__( + 'Privacy-friendly CAPTCHA alternative', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + {enableTurnstile && ( +

    + {__( + 'Configure your Turnstile keys in', + 'airo-wp' + )}{' '} + + {__( + 'Settings → Integrations', + 'airo-wp' + )} + + .{' '} + {__( + 'Widget mode (Managed, Non-interactive, Invisible) is configured in your Cloudflare dashboard.', + 'airo-wp' + )} +

    + )} +
    + + enableEmail !== true} + onDeselect={() => setAttributes({ enableEmail: true })} + isShownByDefault + > + + setAttributes({ enableEmail: value }) + } + help={__( + 'Send email when form is submitted', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + {enableEmail && ( + + {__( + 'Emails are sent using wp_mail(). For reliable delivery, consider using an SMTP plugin like WP Mail SMTP.', + 'airo-wp' + )} + + )} + + + {enableEmail && ( + emailTo !== ''} + onDeselect={() => setAttributes({ emailTo: '' })} + isShownByDefault + > + + setAttributes({ emailTo: value }) + } + type="email" + placeholder="admin@example.com" + help={__( + 'Leave empty to use the site admin email address', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {enableEmail && ( + + emailSubject !== 'New Form Submission' + } + onDeselect={() => + setAttributes({ + emailSubject: 'New Form Submission', + }) + } + isShownByDefault + > + + setAttributes({ emailSubject: value }) + } + help={__( + 'Subject line for notification emails. Use {field_name} for dynamic values.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {enableEmail && ( + emailFromName !== ''} + onDeselect={() => + setAttributes({ emailFromName: '' }) + } + isShownByDefault + > + + setAttributes({ emailFromName: value }) + } + placeholder={__('Site Name', 'airo-wp')} + help={__( + 'Name shown as email sender (leave empty for site name)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {enableEmail && ( + emailFromEmail !== ''} + onDeselect={() => + setAttributes({ emailFromEmail: '' }) + } + isShownByDefault + > + + setAttributes({ emailFromEmail: value }) + } + type="email" + placeholder="wordpress@example.com" + help={__( + 'Email address shown as sender (leave empty for wordpress@yourdomain.com)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {enableEmail && ( + emailReplyTo !== ''} + onDeselect={() => + setAttributes({ emailReplyTo: '' }) + } + isShownByDefault + > + ({ + label: field.label + ? `${field.label} (${field.name})` + : field.name, + value: field.name, + })), + // If the saved value references a field that + // no longer exists, keep it as a selectable + // option so nothing silently changes behavior. + ...(emailReplyTo && + !replyToFieldOptions.some( + (field) => field.name === emailReplyTo + ) + ? [ + { + label: sprintf( + /* translators: %s: missing field name */ + __( + '%s (field not found)', + 'airo-wp' + ), + emailReplyTo + ), + value: emailReplyTo, + }, + ] + : []), + ]} + onChange={(value) => + setAttributes({ emailReplyTo: value }) + } + help={__( + 'Submission from this form field is used as the reply-to address on notification emails.', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {enableEmail && ( + emailBody !== ''} + onDeselect={() => setAttributes({ emailBody: '' })} + isShownByDefault + > + + setAttributes({ emailBody: value }) + } + placeholder={ + __('New form submission:', 'airo-wp') + + '\n\n{all_fields}\n\n' + + __( + 'Submitted from: {page_url}', + 'airo-wp' + ) + } + help={__( + 'Email content template. Use {field_name} for specific fields or {all_fields} for all submitted data.', + 'airo-wp' + )} + rows={5} + __nextHasNoMarginBottom + /> + + )} +
    +
    + + + + setAttributes({ + fieldLabelColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Border Color', 'airo-wp'), + colorValue: decodeColorValue( + fieldBorderColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + fieldBorderColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Field Background', 'airo-wp'), + colorValue: decodeColorValue( + fieldBackgroundColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + fieldBackgroundColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Button Text Color', 'airo-wp'), + colorValue: decodeColorValue( + submitButtonColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + submitButtonColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Button Background Color', 'airo-wp'), + colorValue: decodeColorValue( + submitButtonBackgroundColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + submitButtonBackgroundColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __('Button Hover Text Color', 'airo-wp'), + colorValue: decodeColorValue( + submitButtonHoverColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + submitButtonHoverColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + { + label: __( + 'Button Hover Background Color', + 'airo-wp' + ), + colorValue: decodeColorValue( + submitButtonHoverBackgroundColor, + colorGradientSettings + ), + onColorChange: (color) => + setAttributes({ + submitButtonHoverBackgroundColor: + encodeColorValue( + color, + colorGradientSettings + ) || '', + }), + enableAlpha: true, + clearable: true, + }, + ]} + {...colorGradientSettings} + /> + + +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {submitButtonPosition === 'below' && ( +
    + +
    + )} +
    + + ); +} diff --git a/src/blocks/form-builder/editor.scss b/src/blocks/form-builder/editor.scss new file mode 100644 index 0000000..6c5d213 --- /dev/null +++ b/src/blocks/form-builder/editor.scss @@ -0,0 +1,76 @@ +/** + * Form Builder Block - Editor Styles + * + * CRITICAL: Must be duplicated from style.scss for editor/frontend parity + * + * @since 1.0.0 + */ + +// Import the main frontend styles for base styling +@use './style' as *; + +// Editor-specific overrides and enhancements +.editor-styles-wrapper { + // Form builder in editor + .airo-wp-form-builder { + border: 1px dashed transparent; + transition: border-color 0.2s ease; + + &.is-selected, + &.has-child-selected { + border-color: var(--wp-admin-theme-color, #2563eb); + } + + // Override WP editor default styles on form inputs. + // WP applies styles to input[type="text"], select, textarea etc. + // with .editor-styles-wrapper ancestor specificity. We need higher + // specificity scoped to our form builder to ensure custom properties + // (height, padding, border, background) actually apply. + .airo-wp-form-field__input, + .airo-wp-form-field__textarea, + .airo-wp-form-field__select, + .airo-wp-form-field__country-code { + box-sizing: border-box; + padding: var(--airo-wp-form-input-padding, 0.75rem); + min-height: var(--airo-wp-form-input-height, 44px); + font-size: 1rem; + line-height: 1.5; + color: inherit; + background-color: var(--airo-wp-form-field-bg, var(--wp--preset--color--base, #fff)); + border: 1px solid var(--airo-wp-field-border-color, var(--airo-wp-form-border-color, #d1d5db)); + border-radius: var(--airo-wp-field-border-radius, var(--airo-wp-form-border-radius, 0.375rem)); + } + } + + // Submit button in editor should be disabled + .airo-wp-form__submit { + cursor: default; + pointer-events: none; + + &:hover { + transform: none; + opacity: 1; + } + } + + // Fix inline button alignment in editor. + // The block editor wraps each inner block in a div which shifts flex + // alignment relative to the frontend. Reset the bottom margin so the + // button bottom aligns with the input bottom under align-items: flex-end. + .airo-wp-form-builder--button-inline .airo-wp-form__submit--inline { + margin-bottom: 0; + } + + // Empty state hint for form fields + .airo-wp-form__fields:empty::after { + content: 'Add form fields using the block inserter (+) button'; + display: block; + padding: 2rem; + text-align: center; + color: #6b7280; + font-style: italic; + background-color: #f9fafb; + border: 2px dashed #e5e7eb; + border-radius: var(--airo-wp-form-border-radius); + } +} diff --git a/src/blocks/form-builder/index.js b/src/blocks/form-builder/index.js new file mode 100644 index 0000000..bdd7661 --- /dev/null +++ b/src/blocks/form-builder/index.js @@ -0,0 +1,27 @@ +/** + * Form Builder Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { layout as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-builder/save.js b/src/blocks/form-builder/save.js new file mode 100644 index 0000000..63dc982 --- /dev/null +++ b/src/blocks/form-builder/save.js @@ -0,0 +1,193 @@ +/** + * Form Builder Block - Save Component + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import { validateCSSLength } from '../../utils/css-generator'; + +export default function FormBuilderSave({ attributes }) { + const { + formId, + hasFields, + submitButtonText, + submitButtonAlignment, + submitButtonPosition, + ajaxSubmit, + successMessage, + errorMessage, + fieldSpacing, + inputHeight, + inputPadding, + fieldLabelColor, + fieldBorderColor, + fieldBackgroundColor, + fieldBorderRadius, + submitButtonColor, + submitButtonBackgroundColor, + submitButtonPaddingVertical, + submitButtonPaddingHorizontal, + submitButtonFontSize, + submitButtonHeight, + submitButtonHoverColor, + submitButtonHoverBackgroundColor, + enableHoneypot, + enableTurnstile, + redirectUrl, + } = attributes; + + // If the author never picked a template (so the form has no fields), + // render nothing instead of a lonely submit button. + if (!hasFields) { + return null; + } + + // Same classes as edit.js - MUST MATCH + const formClasses = classnames('airo-wp-form-builder', { + [`airo-wp-form-builder--align-${submitButtonAlignment}`]: + submitButtonAlignment && submitButtonPosition === 'below', + 'airo-wp-form-builder--button-inline': submitButtonPosition === 'inline', + }); + + // Apply form settings as CSS custom properties - MUST MATCH edit.js. + // Spacing/sizing tokens are only written when the author set an explicit + // value; when omitted they fall back to the theme.json custom properties + // (--wp--custom--airo-wp--form--*) defined in style.scss, so a pattern + // can drop them to inherit the theme. + const formStyles = { + ...(fieldSpacing && { '--airo-wp-form-field-spacing': fieldSpacing }), + ...(inputHeight && { '--airo-wp-form-input-height': inputHeight }), + ...(inputPadding && { '--airo-wp-form-input-padding': inputPadding }), + '--airo-wp-form-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-form-border-color': convertColorToCSSVar(fieldBorderColor), + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + '--airo-wp-form-border-radius': validateCSSLength(fieldBorderRadius), + // Button colors now applied as inline styles on button element + }; + + // Submit button style - MUST MATCH edit.js. Sizing (height/padding/font) is + // only written when explicitly set; otherwise the button inherits the + // theme's global button styles via the wp-element-button class. + const submitButtonStyle = { + ...(submitButtonColor && { + color: convertColorToCSSVar(submitButtonColor), + }), + ...(submitButtonBackgroundColor && { + backgroundColor: convertColorToCSSVar(submitButtonBackgroundColor), + }), + ...(submitButtonHeight && { minHeight: submitButtonHeight }), + ...(submitButtonPaddingVertical && { + paddingTop: submitButtonPaddingVertical, + paddingBottom: submitButtonPaddingVertical, + }), + ...(submitButtonPaddingHorizontal && { + paddingLeft: submitButtonPaddingHorizontal, + paddingRight: submitButtonPaddingHorizontal, + }), + ...(submitButtonFontSize && { fontSize: submitButtonFontSize }), + ...(submitButtonHoverBackgroundColor && { + '--airo-wp-button-hover-bg': convertColorToCSSVar( + submitButtonHoverBackgroundColor + ), + }), + ...(submitButtonHoverColor && { + '--airo-wp-button-hover-color': convertColorToCSSVar( + submitButtonHoverColor + ), + }), + }; + + const blockProps = useBlockProps.save({ + className: formClasses, + style: formStyles, + 'data-form-id': formId, + 'data-ajax-submit': ajaxSubmit, + 'data-success-message': successMessage, + 'data-error-message': errorMessage, + 'data-submit-text': submitButtonText, + ...(enableTurnstile && { + 'data-airo-wp-turnstile': 'true', + }), + ...(redirectUrl && { + 'data-redirect-url': redirectUrl, + }), + }); + + // Extract children from innerBlocksProps so we can add button inside fields container + const { children, ...innerBlocksPropsWithoutChildren } = + useInnerBlocksProps.save({ + className: 'airo-wp-form__fields', + }); + + return ( +
    +
    +
    + {children} + {submitButtonPosition === 'inline' && ( + + )} +
    + + {enableHoneypot && ( + + )} + + + + {/* Timestamp added via JavaScript in view.js to avoid validation errors */} + + {/* Turnstile widget container - rendered by JS */} + {enableTurnstile && ( +
    + )} + + {submitButtonPosition === 'below' && ( +
    + +
    + )} + +
    + +
    + ); +} diff --git a/src/blocks/form-builder/style.scss b/src/blocks/form-builder/style.scss new file mode 100644 index 0000000..5c76e04 --- /dev/null +++ b/src/blocks/form-builder/style.scss @@ -0,0 +1,564 @@ +/** + * Form Builder Block - Frontend Styles + * + * @since 1.0.0 + */ + +.airo-wp-form-builder { + // CSS Custom Properties. These are the inherited defaults: each reads a + // theme.json custom property (settings.custom.airowp.form.*, which a + // Style Kit can set) and falls back to a sane value. When the author sets an + // explicit value on the block it is written as an inline style that overrides + // the token here; when omitted, the form inherits the theme. + --airo-wp-form-field-spacing: var( + --wp--custom--airo-wp--form--field-spacing, + 1.5rem + ); + --airo-wp-form-input-height: var( + --wp--custom--airo-wp--form--input-height, + 44px + ); + --airo-wp-form-input-padding: var( + --wp--custom--airo-wp--form--input-padding, + 0.75rem + ); + --airo-wp-form-label-color: inherit; + --airo-wp-form-border-color: #d1d5db; + --airo-wp-form-border-radius: 0.375rem; + --airo-wp-form-focus-color: var(--wp--preset--color--primary, #2563eb); + --airo-wp-form-error-color: #dc2626; + --airo-wp-form-success-color: #16a34a; + // Button colors intentionally not set here - uses theme defaults unless overridden + + // The wrapper itself must never render a border. WordPress core ships + // `html :where([style*="border-color"]) { border-style: solid; }`, whose + // substring match catches the `--airo-wp-form-border-color` custom property in + // this block's inline style and forces a solid border (with currentColor and + // a default width) around the whole form. The border color is only ever + // consumed by the field inputs, never the wrapper, so reset it here. + border-style: none; + + // Prevents padding + border from exceeding container width when border-radius is applied + box-sizing: border-box; + padding: 1rem; + width: 100%; + max-width: 100%; +} + +.airo-wp-form { + display: flex; + flex-direction: column; + gap: var(--airo-wp-form-field-spacing); + width: 100%; + + // Form fields container + &__fields { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--airo-wp-form-field-spacing); + align-items: flex-start; + } + + // Footer with submit button + &__footer { + display: flex; + margin-top: var(--airo-wp-form-field-spacing); + } + + // Submit button - hover behavior mirrors icon-button defaults + &__submit { + // No color/background rules - let theme handle via wp-element-button. + // Custom colors and sizing applied as inline styles via block attributes. + // Intentional asymmetry with the field tokens above: those custom + // properties fall back to the old hardcoded pixel defaults, so migrated + // forms stay pixel-stable. The submit button deliberately has NO such + // fallback — when its height/padding attributes are unset (or stripped on + // migration) it inherits the theme's global button styling (wp-element-button) + // rather than a baked 44px, which is the requested behaviour. + + // Hover animation support + overflow: hidden; + position: relative; + text-decoration: none !important; + transition: all 0.2s ease; + cursor: pointer; + + &:hover { + opacity: 0.9; + transform: translateY(-1px); + text-decoration: none !important; + } + + &:active { + transform: translateY(0); + text-decoration: none !important; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + + &:hover { + transform: none; + opacity: 0.5; + } + } + + // Inline button (for subscribe forms, etc.) + // Sizing is controlled via block attributes (inline styles) + &--inline { + flex: 0 0 auto; + width: auto; + } + + // Loading state + &.airo-wp-form__submit--loading { + color: transparent; + pointer-events: none; + + &::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid currentcolor; + border-radius: 50%; + border-top-color: transparent; + animation: airo-wp-form-spinner 0.6s linear infinite; + color: inherit; + } + } + } + + // Message container (success/error) + &__message { + margin-top: 1rem; + padding: 1rem; + border-radius: var(--airo-wp-form-border-radius); + font-size: 0.875rem; + line-height: 1.5; + + &--success { + color: var(--airo-wp-form-success-color); + background-color: rgb(220 252 231); // light green + border: 1px solid var(--airo-wp-form-success-color); + } + + &--error { + color: var(--airo-wp-form-error-color); + background-color: rgb(254 226 226); // light red + border: 1px solid var(--airo-wp-form-error-color); + } + } +} + +// Turnstile widget container +.airo-wp-turnstile-widget { + margin-top: var(--airo-wp-form-field-spacing); + width: 100%; +} + +// Button alignment modifiers +.airo-wp-form-builder--align-left { + + .airo-wp-form__footer { + justify-content: flex-start; + } +} + +.airo-wp-form-builder--align-center { + + .airo-wp-form__footer { + justify-content: center; + } +} + +.airo-wp-form-builder--align-right { + + .airo-wp-form__footer { + justify-content: flex-end; + } +} + +// Inline button mode (for subscribe forms) +.airo-wp-form-builder--button-inline { + + .airo-wp-form__fields { + align-items: flex-end; + } + + // Auto-size the last form field to share its row with the button. + // Overrides any explicit field width so users don't need to manually + // shrink the last field when switching to inline mode. + .airo-wp-form-field:last-of-type { + flex: 1 1 0 !important; + max-width: none !important; + + // Mobile: force full width so the button wraps to a new line + @media (max-width: 768px) { + flex-basis: 100% !important; + } + } + + .airo-wp-form__submit--inline { + flex: 0 0 auto; + width: auto; + // Align button bottom with input bottom — compensates for the + // form field's internal label-to-input gap (0.5rem) + margin-bottom: 0.5rem; + + // Mobile: stack below the field with spacing + @media (max-width: 768px) { + margin-bottom: 0; + margin-top: 0.5rem; + } + } + + .airo-wp-form__footer { + display: none; + } +} + +// Loading spinner animation +@keyframes airo-wp-form-spinner { + + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +// Responsive adjustments +@media (max-width: 768px) { + + .airo-wp-form__submit { + width: 100%; // Full width on mobile for easier tapping + } + + .airo-wp-form-builder--align-left, + .airo-wp-form-builder--align-center, + .airo-wp-form-builder--align-right { + + .airo-wp-form__footer { + justify-content: stretch; + } + } + +} + +// Print styles +@media print { + + .airo-wp-form__submit { + display: none; + } + + .airo-wp-form__message { + display: none; + } +} + +// ============================================================================ +// HOVER COLORS +// ============================================================================ +// Custom hover colors via CSS variables (same pattern as icon-button) + +// No animation — instant background transition +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"]:not([class*="airo-wp-form__submit--"]):hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: background-color 0.3s ease; +} + +// Zoom In — preserve transform transition +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--zoom-in:hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: transform 0.3s ease, background-color 0.3s ease; +} + +// Lift — preserve transform and box-shadow transitions +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--lift:hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: transform 0.5s ease, box-shadow 0.5s ease, background-color 0.3s ease; +} + +// Shrink — preserve transform transition +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--shrink:hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: transform 0.4s ease, background-color 0.3s ease; +} + +// Border Pulse — preserve border-width transition +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--border-pulse:hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: border-width 0.3s ease, background-color 0.3s ease; +} + +// Border Glow — preserve box-shadow transition +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--border-glow:hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: all 0.3s ease, background-color 0.3s ease; +} + +// Slide animations — fade in after slide completes +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--slide-left:hover, +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--slide-right:hover, +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--slide-down:hover, +.airo-wp-form__submit[style*="--airo-wp-button-hover-bg"].airo-wp-form__submit--slide-up:hover { + background-color: var(--airo-wp-button-hover-bg) !important; + opacity: 1; + transition: background-color 0.1s ease 0.3s; +} + +// Hover text color +.airo-wp-form__submit[style*="--airo-wp-button-hover-color"]:hover { + color: var(--airo-wp-button-hover-color) !important; + transition: color 0.3s ease; +} + +// ============================================================================ +// HOVER ANIMATIONS +// ============================================================================ + +// Fill Diagonal — Diagonal sweep fill effect +.airo-wp-form__submit.airo-wp-form__submit--fill-diagonal { + + &:hover { + opacity: 1; + transform: none; + } + + &::before { + content: ""; + position: absolute; + top: 0; + left: -150%; + width: 150%; + height: 100%; + background-color: var(--airo-wp-button-hover-bg, rgba(0, 0, 0, 0.15)); + transition: left 0.5s ease; + z-index: 0; + transform: skewX(-20deg); + transform-origin: top left; + } + + // Button text above pseudo-element + > * { + position: relative; + z-index: 1; + } + + &:hover::before { + left: 0; + } +} + +// Zoom In — Scale up on hover +.airo-wp-form__submit.airo-wp-form__submit--zoom-in { + transform: scale(1); + transition: transform 0.3s ease; + + &:hover { + transform: scale(1.05); + opacity: 1; + } +} + +// Slide Left — Background slides in from right +.airo-wp-form__submit.airo-wp-form__submit--slide-left { + + &:hover { + opacity: 1; + transform: none; + } + + &::before { + content: ""; + position: absolute; + top: 0; + right: -100%; + width: 100%; + height: 100%; + background-color: var(--airo-wp-button-hover-bg, rgba(0, 0, 0, 0.15)); + transition: right 0.3s ease; + z-index: 0; + } + + > * { + position: relative; + z-index: 1; + } + + &:hover::before { + right: 0; + } +} + +// Slide Right — Background slides in from left +.airo-wp-form__submit.airo-wp-form__submit--slide-right { + + &:hover { + opacity: 1; + transform: none; + } + + &::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background-color: var(--airo-wp-button-hover-bg, rgba(0, 0, 0, 0.15)); + transition: left 0.3s ease; + z-index: 0; + } + + > * { + position: relative; + z-index: 1; + } + + &:hover::before { + left: 0; + } +} + +// Slide Down — Background slides down from top +.airo-wp-form__submit.airo-wp-form__submit--slide-down { + + &:hover { + opacity: 1; + transform: none; + } + + &::before { + content: ""; + position: absolute; + top: -100%; + left: 0; + width: 100%; + height: 100%; + background-color: var(--airo-wp-button-hover-bg, rgba(0, 0, 0, 0.15)); + transition: top 0.3s ease; + z-index: 0; + } + + > * { + position: relative; + z-index: 1; + } + + &:hover::before { + top: 0; + } +} + +// Slide Up — Background slides up from bottom +.airo-wp-form__submit.airo-wp-form__submit--slide-up { + + &:hover { + opacity: 1; + transform: none; + } + + &::before { + content: ""; + position: absolute; + bottom: -100%; + left: 0; + width: 100%; + height: 100%; + background-color: var(--airo-wp-button-hover-bg, rgba(0, 0, 0, 0.15)); + transition: bottom 0.3s ease; + z-index: 0; + } + + > * { + position: relative; + z-index: 1; + } + + &:hover::before { + bottom: 0; + } +} + +// Border Pulse — Animated border effect using box-shadow +.airo-wp-form__submit.airo-wp-form__submit--border-pulse { + transition: box-shadow 0.3s ease; + + &:hover { + animation: airo-wp-form-border-pulse 1s ease infinite; + opacity: 1; + transform: none; + } +} + +@keyframes airo-wp-form-border-pulse { + + 0%, + 100% { + box-shadow: 0 0 0 0 currentcolor; + } + + 50% { + box-shadow: 0 0 0 4px currentcolor; + } +} + +// Border Glow — Glowing border effect +.airo-wp-form__submit.airo-wp-form__submit--border-glow { + transition: all 0.3s ease; + + &:hover { + opacity: 1; + transform: none; + box-shadow: 0 0 15px currentcolor, 0 0 30px currentcolor, 0 0 45px currentcolor; + } +} + +// Lift — Elevate with shadow +.airo-wp-form__submit.airo-wp-form__submit--lift { + transform: translateY(0); + transition: transform 0.5s ease, box-shadow 0.5s ease; + + &:hover { + transform: translateY(-8px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), 0 6px 12px rgba(0, 0, 0, 0.15); + opacity: 1; + } +} + +// Shrink — Subtle scale down +.airo-wp-form__submit.airo-wp-form__submit--shrink { + transform: scale(1); + transition: transform 0.4s ease; + + &:hover { + transform: scale(0.95); + opacity: 1; + } +} + +// No Hover — Explicit opt-out from hover animations +.airo-wp-form__submit.airo-wp-form__submit--no-hover { + + &:hover { + opacity: 1; + transform: none; + } +} diff --git a/src/blocks/form-builder/templates.js b/src/blocks/form-builder/templates.js new file mode 100644 index 0000000..34c9dbd --- /dev/null +++ b/src/blocks/form-builder/templates.js @@ -0,0 +1,208 @@ +/** + * Form Builder Templates + * + * Preset structures surfaced in the first-insert placeholder. + * Each template seeds its own innerBlocks and any notification/messaging + * defaults appropriate for the use case. + */ + +import { __ } from '@wordpress/i18n'; + +export const formBuilderTemplates = [ + { + name: 'blank', + title: __('Blank', 'airo-wp'), + description: __('Start with a single text field', 'airo-wp'), + icon: 'welcome-add-page', + attributes: {}, + innerBlocks: [ + [ + 'airo-wp/form-text-field', + { + label: __('Name', 'airo-wp'), + fieldName: 'name', + required: true, + }, + ], + ], + }, + { + name: 'contact', + title: __('Contact', 'airo-wp'), + description: __( + 'Name, email, and message — the classic contact form', + 'airo-wp' + ), + icon: 'email', + attributes: { + submitButtonText: __('Send Message', 'airo-wp'), + successMessage: __( + "Thanks — we'll be in touch shortly.", + 'airo-wp' + ), + enableEmail: true, + emailSubject: __('New contact form submission', 'airo-wp'), + emailReplyTo: 'email', + }, + innerBlocks: [ + [ + 'airo-wp/form-text-field', + { + label: __('Name', 'airo-wp'), + fieldName: 'name', + required: true, + }, + ], + [ + 'airo-wp/form-email-field', + { + label: __('Email', 'airo-wp'), + fieldName: 'email', + required: true, + }, + ], + [ + 'airo-wp/form-textarea-field', + { + label: __('Message', 'airo-wp'), + fieldName: 'message', + required: true, + }, + ], + ], + }, + { + name: 'newsletter', + title: __('Newsletter', 'airo-wp'), + description: __( + 'Single email field with an inline subscribe button', + 'airo-wp' + ), + icon: 'email-alt', + attributes: { + submitButtonText: __('Subscribe', 'airo-wp'), + submitButtonPosition: 'inline', + successMessage: __('Thanks for subscribing!', 'airo-wp'), + enableEmail: true, + emailSubject: __('New newsletter signup', 'airo-wp'), + emailReplyTo: 'email', + }, + innerBlocks: [ + [ + 'airo-wp/form-email-field', + { + label: __('Email Address', 'airo-wp'), + fieldName: 'email', + required: true, + placeholder: __('you@example.com', 'airo-wp'), + }, + ], + ], + }, + { + name: 'event-registration', + title: __('Event Registration', 'airo-wp'), + description: __( + 'Name, email, phone, and number of guests', + 'airo-wp' + ), + icon: 'calendar-alt', + attributes: { + submitButtonText: __('Register', 'airo-wp'), + successMessage: __( + "You're on the list — check your inbox for details.", + 'airo-wp' + ), + enableEmail: true, + emailSubject: __('New event registration', 'airo-wp'), + emailReplyTo: 'email', + }, + innerBlocks: [ + [ + 'airo-wp/form-text-field', + { + label: __('Full Name', 'airo-wp'), + fieldName: 'name', + required: true, + }, + ], + [ + 'airo-wp/form-email-field', + { + label: __('Email', 'airo-wp'), + fieldName: 'email', + required: true, + }, + ], + [ + 'airo-wp/form-phone-field', + { + label: __('Phone', 'airo-wp'), + fieldName: 'phone', + }, + ], + [ + 'airo-wp/form-number-field', + { + label: __('Number of Guests', 'airo-wp'), + fieldName: 'guests', + min: 1, + max: 10, + defaultValue: 1, + }, + ], + ], + }, + { + name: 'lead-capture', + title: __('Lead Capture', 'airo-wp'), + description: __( + 'Name, work email, company, and phone for B2B leads', + 'airo-wp' + ), + icon: 'businessman', + attributes: { + submitButtonText: __('Get in Touch', 'airo-wp'), + successMessage: __( + "Thanks — we'll reach out shortly.", + 'airo-wp' + ), + enableEmail: true, + emailSubject: __('New lead submission', 'airo-wp'), + emailReplyTo: 'email', + }, + innerBlocks: [ + [ + 'airo-wp/form-text-field', + { + label: __('Full Name', 'airo-wp'), + fieldName: 'name', + required: true, + }, + ], + [ + 'airo-wp/form-email-field', + { + label: __('Work Email', 'airo-wp'), + fieldName: 'email', + required: true, + }, + ], + [ + 'airo-wp/form-text-field', + { + label: __('Company', 'airo-wp'), + fieldName: 'company', + required: true, + }, + ], + [ + 'airo-wp/form-phone-field', + { + label: __('Phone', 'airo-wp'), + fieldName: 'phone', + }, + ], + ], + }, +]; diff --git a/src/blocks/form-builder/view.js b/src/blocks/form-builder/view.js new file mode 100644 index 0000000..c19f5ca --- /dev/null +++ b/src/blocks/form-builder/view.js @@ -0,0 +1,692 @@ +/** + * Form Builder Block - Frontend JavaScript + * + * Handles AJAX form submission with validation + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; + +/* global airowpForm, dsgoIntegrations, sessionStorage */ + +// Track Turnstile script loading state +let turnstileScriptLoaded = false; +let turnstileScriptLoading = false; +const turnstileLoadCallbacks = []; + +/** + * Load Turnstile script dynamically + * + * @return {Promise} Resolves when script is loaded + */ +function loadTurnstileScript() { + return new Promise((resolve, reject) => { + // Already loaded + if (turnstileScriptLoaded && window.turnstile) { + resolve(); + return; + } + + // Currently loading - queue callback + if (turnstileScriptLoading) { + turnstileLoadCallbacks.push({ resolve, reject }); + return; + } + + turnstileScriptLoading = true; + + const script = document.createElement('script'); + script.src = + 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + script.defer = true; + + script.onload = () => { + turnstileScriptLoaded = true; + turnstileScriptLoading = false; + resolve(); + // Resolve queued callbacks + turnstileLoadCallbacks.forEach((cb) => cb.resolve()); + turnstileLoadCallbacks.length = 0; + }; + + script.onerror = () => { + turnstileScriptLoading = false; + const error = new Error('Failed to load Turnstile script'); + reject(error); + // Reject queued callbacks + turnstileLoadCallbacks.forEach((cb) => cb.reject(error)); + turnstileLoadCallbacks.length = 0; + }; + + document.head.appendChild(script); + }); +} + +function initFormBuilder() { + const forms = document.querySelectorAll('.airo-wp-form-builder'); + + forms.forEach((formContainer) => { + // Guard against duplicate initialization (e.g. bfcache restore) + if (formContainer.dataset.dsgoInitialized) { + return; + } + formContainer.dataset.dsgoInitialized = 'true'; + + const formElement = formContainer.querySelector('.airo-wp-form'); + const submitButton = formElement?.querySelector('.airo-wp-form__submit'); + const messageContainer = formElement?.querySelector( + '.airo-wp-form__message' + ); + + if (!formElement || !submitButton || !messageContainer) { + return; + } + + // Add timestamp field dynamically (not in save.js to avoid validation errors) + // Set at init time so server can verify user spent >= 3s on page (anti-spam). + // Re-set on bfcache restore so stale timestamps don't cause false rejections. + const timestampField = document.createElement('input'); + timestampField.type = 'hidden'; + timestampField.name = 'dsg_timestamp'; + timestampField.value = Date.now(); + formElement.appendChild(timestampField); + + window.addEventListener('pageshow', function (e) { + if (e.persisted) { + timestampField.value = Date.now(); + } + }); + + // Turnstile state for this form + let turnstileToken = null; + let turnstileWidgetId = null; + + // Check if Turnstile is enabled for this form + const turnstileEnabled = + formContainer.getAttribute('data-airo-wp-turnstile') === 'true'; + const turnstileContainer = formElement.querySelector( + '[data-airo-wp-turnstile-container]' + ); + const turnstileSiteKey = + typeof dsgoIntegrations !== 'undefined' + ? dsgoIntegrations.turnstileSiteKey + : null; + let turnstileTokenField = null; + + if (turnstileEnabled) { + turnstileTokenField = document.createElement('input'); + turnstileTokenField.type = 'hidden'; + turnstileTokenField.name = 'dsg_turnstile_token'; + formElement.appendChild(turnstileTokenField); + } + + // Initialize Turnstile if enabled + if (turnstileEnabled && turnstileContainer && turnstileSiteKey) { + loadTurnstileScript() + .then(() => { + if (!window.turnstile) { + throw new Error('Turnstile not available'); + } + + turnstileWidgetId = window.turnstile.render( + turnstileContainer, + { + sitekey: turnstileSiteKey, + theme: 'auto', + size: 'normal', + // Mode (managed/non-interactive/invisible) is configured in Cloudflare dashboard + callback: (token) => { + turnstileToken = token; + if (turnstileTokenField) { + turnstileTokenField.value = token; + } + }, + 'expired-callback': () => { + turnstileToken = null; + if (turnstileTokenField) { + turnstileTokenField.value = ''; + } + // Reset widget for new token + if ( + turnstileWidgetId !== null && + window.turnstile + ) { + window.turnstile.reset(turnstileWidgetId); + } + }, + 'error-callback': () => { + turnstileToken = null; + if (turnstileTokenField) { + turnstileTokenField.value = ''; + } + // Graceful degradation - form will submit without Turnstile token + // eslint-disable-next-line no-console -- Log for debugging + console.warn( + 'Turnstile widget error - form will submit without verification' + ); + }, + } + ); + }) + .catch((error) => { + turnstileToken = null; + if (turnstileTokenField) { + turnstileTokenField.value = ''; + } + // Graceful degradation - form will submit without Turnstile token + // eslint-disable-next-line no-console -- Log for debugging + console.warn('Turnstile failed to load:', error.message); + }); + } + + const formId = formContainer.getAttribute('data-form-id'); + const storageKey = (() => { + if (formId) { + return `airowp_confirmed_${formId}`; + } + if (formContainer.id) { + return `airowp_confirmed_instance_${formContainer.id.replace(/[^a-zA-Z0-9_-]/g, '_')}`; + } + const idx = Array.from( + document.querySelectorAll('[data-form-id]') + ).indexOf(formContainer); + return `airowp_confirmed_instance_${idx >= 0 ? idx : 0}`; + })(); + const successMessage = formContainer.getAttribute( + 'data-success-message' + ); + const errorMessage = formContainer.getAttribute('data-error-message'); + + function ensureNativePostFields() { + formElement.action = airowpForm.adminPostUrl; + formElement.method = 'post'; + + let actionField = formElement.querySelector('input[name="action"]'); + if (!actionField) { + actionField = document.createElement('input'); + actionField.type = 'hidden'; + actionField.name = 'action'; + formElement.appendChild(actionField); + } + actionField.value = 'airowp_form_submit'; + actionField.defaultValue = actionField.value; + + let nonceField = formElement.querySelector( + 'input[name="_wpnonce"]' + ); + if (!nonceField) { + nonceField = document.createElement('input'); + nonceField.type = 'hidden'; + nonceField.name = '_wpnonce'; + formElement.appendChild(nonceField); + } + nonceField.value = airowpForm.ajaxNonce; + nonceField.defaultValue = nonceField.value; + } + + function getStoredConfirmation() { + try { + return sessionStorage.getItem(storageKey); + } catch { + return null; + } + } + + function storeConfirmation(message) { + try { + sessionStorage.setItem(storageKey, message); + } catch { + // sessionStorage unavailable + } + } + + function clearConfirmation() { + try { + sessionStorage.removeItem(storageKey); + } catch { + // sessionStorage unavailable + } + } + + function showRedirectStatus() { + const params = new URLSearchParams(window.location.search); + const status = params.get('airowp_form_status'); + const statusFormId = params.get('airowp_form_id'); + const matchesCurrentForm = !statusFormId || statusFormId === formId; + let shown = false; + + if ( + matchesCurrentForm && + (params.has('airowp_form_success') || status === 'success') + ) { + const msg = + successMessage || + __('Form submitted successfully!', 'airo-wp'); + showMessage(messageContainer, msg, 'success'); + storeConfirmation(msg); + formElement.reset(); + shown = true; + } else if ( + matchesCurrentForm && + (params.has('airowp_form_error') || status === 'error') + ) { + clearConfirmation(); + showMessage( + messageContainer, + errorMessage || + __( + 'An error occurred. Please try again.', + 'airo-wp' + ), + 'error' + ); + shown = true; + } + + // Strip form status params from URL to prevent stale messages on refresh + if (shown) { + const cleanUrl = new URL(window.location.href); + cleanUrl.searchParams.delete('airowp_form_success'); + cleanUrl.searchParams.delete('airowp_form_error'); + cleanUrl.searchParams.delete('airowp_form_status'); + cleanUrl.searchParams.delete('airowp_form_id'); + window.history.replaceState(null, '', cleanUrl.href); + } + + return shown; + } + + function markTransportBlocked(key) { + try { + sessionStorage.setItem(key, '1'); + } catch { + // sessionStorage may be unavailable + } + } + + function isTransportBlocked(key) { + try { + return sessionStorage.getItem(key) === '1'; + } catch { + return false; + } + } + + function submitViaNativePost() { + ensureNativePostFields(); + if (turnstileTokenField) { + turnstileTokenField.value = turnstileToken || ''; + } + + // Build field type map so server can validate/sanitize correctly + const typeMap = {}; + formElement.querySelectorAll('[data-field-type]').forEach((el) => { + if (el.name) { + typeMap[el.name] = el.getAttribute('data-field-type'); + } + }); + let typeMapField = formElement.querySelector( + 'input[name="dsg_field_types"]' + ); + if (!typeMapField) { + typeMapField = document.createElement('input'); + typeMapField.type = 'hidden'; + typeMapField.name = 'dsg_field_types'; + formElement.appendChild(typeMapField); + } + typeMapField.value = JSON.stringify(typeMap); + + window.HTMLFormElement.prototype.submit.call(formElement); + } + + const shownFromRedirect = showRedirectStatus(); + + // Restore confirmation message persisted from a previous submission (one-time) + if (!shownFromRedirect) { + const storedConfirmation = getStoredConfirmation(); + if (storedConfirmation) { + showMessage(messageContainer, storedConfirmation, 'success'); + clearConfirmation(); + } + } + + // Check if AJAX is enabled + const ajaxEnabled = + formContainer.getAttribute('data-ajax-submit') === 'true'; + + // Non-AJAX: set up standard form POST to admin_post + if (!ajaxEnabled) { + ensureNativePostFields(); + + return; + } + + const safeRedirectUrl = getSafeRedirectUrl( + formContainer.getAttribute('data-redirect-url') + ); + + // Handle form submission + formElement.addEventListener('submit', async function (e) { + e.preventDefault(); + + // Clear previous messages and any persisted confirmation state + hideMessage(messageContainer); + clearConfirmation(); + + // Validate form using HTML5 validation + if (!formElement.checkValidity()) { + formElement.reportValidity(); + return; + } + + // Disable submit button and show loading state + submitButton.disabled = true; + submitButton.classList.add('airo-wp-form__submit--loading'); + const originalText = submitButton.textContent; + submitButton.setAttribute('aria-busy', 'true'); + + // Collect form data + const formData = new FormData(formElement); + const fields = []; + + for (const [name, value] of formData.entries()) { + // Skip honeypot and system fields + if ( + name === 'dsg_website' || + name === 'dsg_form_id' || + name === 'dsg_timestamp' || + name === 'dsg_turnstile_token' + ) { + continue; + } + + const fieldElement = formElement.querySelector( + `[name="${name}"]` + ); + const fieldType = + fieldElement?.getAttribute('data-field-type') || + fieldElement?.type || + 'text'; + + fields.push({ + name, + value, + type: fieldType, + }); + } + + // Get honeypot and timestamp values + const honeypot = formData.get('dsg_website'); + const timestamp = formData.get('dsg_timestamp'); + + let redirecting = false; + + try { + const requestBody = JSON.stringify({ + formId, + fields, + honeypot: honeypot || '', + timestamp: timestamp || Date.now(), + // Include Turnstile token if available (graceful degradation: empty if failed) + turnstile_token: turnstileToken || '', + }); + + let result; + + // Submit via admin-ajax (form-encoded) or REST API. + // Some hosts (GoDaddy/Cloudflare) block JSON POSTs entirely, + // so we use admin-ajax as primary when available, with REST + // as fallback. sessionStorage remembers if REST failed before + // to avoid wasting the rate limit window on a doomed request. + const useAjax = + airowpForm.ajaxUrl && airowpForm.ajaxNonce; + const restBlocked = + useAjax && isTransportBlocked('airowp_rest_blocked'); + const ajaxBlocked = + useAjax && isTransportBlocked('airowp_ajax_blocked'); + + if (ajaxBlocked) { + redirecting = true; + submitViaNativePost(); + return; + } + + if (!restBlocked) { + // Try REST API first + const restResponse = await fetch(airowpForm.restUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': airowpForm.nonce, + }, + body: requestBody, + }); + + if (restResponse.ok) { + result = await restResponse.json(); + } else if (restResponse.status === 429 && useAjax) { + // Remember that REST is blocked for this session + markTransportBlocked('airowp_rest_blocked'); + // Fall through to admin-ajax below + } else { + // Non-429 error from REST API + let serverMessage; + try { + const errorData = await restResponse.json(); + serverMessage = errorData.message; + } catch { + // Response body isn't valid JSON + } + + throw new Error( + serverMessage || + __( + 'The server returned an unexpected response. Please try again later.', + 'airo-wp' + ) + ); + } + } + + // Admin-ajax fallback (or primary if REST was blocked) + if (!result && useAjax) { + const ajaxBody = new URLSearchParams(); + ajaxBody.set('action', 'airowp_form_submit'); + ajaxBody.set('_ajax_nonce', airowpForm.ajaxNonce); + ajaxBody.set('form_data', requestBody); + + const ajaxResponse = await fetch(airowpForm.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: ajaxBody.toString(), + }); + + if (!ajaxResponse.ok) { + if (ajaxResponse.status === 429) { + markTransportBlocked('airowp_ajax_blocked'); + redirecting = true; + submitViaNativePost(); + return; + } + + let ajaxMessage; + try { + const ajaxError = await ajaxResponse.json(); + ajaxMessage = + ajaxError.data?.message || ajaxError.message; + } catch { + // Non-JSON response + } + + throw new Error( + ajaxMessage || + __( + 'The server returned an unexpected response. Please try again later.', + 'airo-wp' + ) + ); + } + + const ajaxResult = await ajaxResponse.json(); + // wp_send_json_success wraps in { success, data } + result = ajaxResult.data; + } + + if (result.success) { + // Fire custom event for tracking/analytics + formContainer.dispatchEvent( + new CustomEvent('dsgoFormSubmitted', { + detail: { + formId, + submissionId: result.submissionId, + }, + bubbles: true, + }) + ); + + // Redirect using the normalized safe URL instead of the raw + // DOM attribute value so the browser never reparses unsanitized text. + if (safeRedirectUrl) { + redirecting = true; + window.location.assign(safeRedirectUrl); + return; + } + + // Show success message and persist for reload + const successMsg = successMessage || result.message; + showMessage(messageContainer, successMsg, 'success'); + storeConfirmation(successMsg); + + // Reset form + formElement.reset(); + turnstileToken = null; + if (turnstileTokenField) { + turnstileTokenField.value = ''; + } + if (turnstileWidgetId !== null && window.turnstile) { + window.turnstile.reset(turnstileWidgetId); + } + + // Scroll to message if not visible + if (!isElementInViewport(messageContainer)) { + messageContainer.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }); + } + } else { + throw new Error(result.message || errorMessage); + } + } catch (error) { + // eslint-disable-next-line no-console -- Error logging for debugging + console.error('Form submission error:', error); + + // Show error message + showMessage( + messageContainer, + error.message || + errorMessage || + 'An error occurred. Please try again.', + 'error' + ); + + // Fire custom event for error tracking + formContainer.dispatchEvent( + new CustomEvent('dsgoFormError', { + detail: { + formId, + error: error.message, + }, + bubbles: true, + }) + ); + } finally { + // Skip button reset if navigating away (redirect) + if (!redirecting) { + submitButton.disabled = false; + submitButton.classList.remove('airo-wp-form__submit--loading'); + submitButton.textContent = originalText; + submitButton.removeAttribute('aria-busy'); + } + } + }); + }); + + /** + * Show message to user + * + * @param {HTMLElement} container Message container element + * @param {string} message Message text to display + * @param {string} type Message type: 'success' or 'error' + */ + function showMessage(container, message, type) { + container.textContent = message; + container.className = `airo-wp-form__message airo-wp-form__message--${type}`; + container.style.display = 'block'; + container.setAttribute('role', type === 'error' ? 'alert' : 'status'); + + // Announce to screen readers + const announcement = document.createElement('span'); + announcement.className = 'screen-reader-text'; + announcement.textContent = message; + announcement.setAttribute('aria-live', 'polite'); + container.appendChild(announcement); + } + + /** + * Hide message container + * + * @param {HTMLElement} container Message container element + */ + function hideMessage(container) { + container.style.display = 'none'; + container.textContent = ''; + container.className = 'airo-wp-form__message'; + } + + /** + * Return a canonical redirect URL when it uses an allowed protocol. + * + * @param {string|null} url URL to validate + * @return {string|null} Canonical URL or null when unsafe + */ + function getSafeRedirectUrl(url) { + if (!url) { + return null; + } + + try { + const parsed = new URL(url, window.location.origin); + return parsed.protocol === 'http:' || parsed.protocol === 'https:' + ? parsed.toString() + : null; + } catch { + return null; + } + } + + /** + * Check if element is in viewport + * + * @param {HTMLElement} element Element to check + * @return {boolean} True if element is in viewport + */ + function isElementInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= + (window.innerWidth || document.documentElement.clientWidth) + ); + } +} + +document.addEventListener('DOMContentLoaded', initFormBuilder); +document.addEventListener('airo-wp-content-loaded', initFormBuilder); diff --git a/src/blocks/form-checkbox-field/block.json b/src/blocks/form-checkbox-field/block.json new file mode 100644 index 0000000..c843078 --- /dev/null +++ b/src/blocks/form-checkbox-field/block.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-checkbox-field", + "version": "1.0.0", + "title": "Checkbox Field", + "category": "airo-wp", + "description": "A single checkbox field for forms.", + "keywords": [ + "form", + "checkbox", + "check", + "toggle", + "agree" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "yes-alt", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "usesContext": [ + "airo-wp/form/fieldSpacing", + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "I agree to the terms and conditions" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "checkedByDefault": { + "type": "boolean", + "default": false + }, + "value": { + "type": "string", + "default": "1" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-checkbox-field/deprecated.js b/src/blocks/form-checkbox-field/deprecated.js new file mode 100644 index 0000000..6332dbb --- /dev/null +++ b/src/blocks/form-checkbox-field/deprecated.js @@ -0,0 +1,125 @@ +/** + * Form Checkbox Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static + * checkbox field so existing content — and the current block-patterns HTML — + * migrates silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.5.0 + */ + +import { useBlockProps, RichText } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports. + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attribute schema for the static deprecation (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'I agree to the terms and conditions' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + checkedByDefault: { type: 'boolean', default: false }, + value: { type: 'string', default: '1' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static checkbox field carries this wrapper class; the + // dynamic block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && + innerHTML.includes('airo-wp-form-field--checkbox') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + helpText, + required, + checkedByDefault, + value, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--checkbox' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    +
    + + +
    + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic]; diff --git a/src/blocks/form-checkbox-field/edit.js b/src/blocks/form-checkbox-field/edit.js new file mode 100644 index 0000000..09c36bc --- /dev/null +++ b/src/blocks/form-checkbox-field/edit.js @@ -0,0 +1,223 @@ +/** + * Form Checkbox Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + InspectorControls, + RichText, +} from '@wordpress/block-editor'; +import { TextControl, ToggleControl } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; + +const DEFAULT_LABEL = 'I agree to the terms and conditions'; +const DEFAULT_VALUE = '1'; + +export default function FormCheckboxFieldEdit({ + attributes, + setAttributes, + clientId, +}) { + const { fieldName, label, helpText, required, checkedByDefault, value } = + attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `checkbox-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--checkbox' + ); + + const blockProps = useBlockProps({ + className: fieldClasses, + }); + + const fieldId = `field-${fieldName}`; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: DEFAULT_LABEL, + helpText: '', + required: false, + checkedByDefault: false, + value: DEFAULT_VALUE, + }) + } + > + + !!fieldName && + !/^checkbox-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: newValue.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: newValue }) + } + __nextHasNoMarginBottom + /> + + + checkedByDefault !== false} + onDeselect={() => + setAttributes({ checkedByDefault: false }) + } + isShownByDefault + > + + setAttributes({ checkedByDefault: newValue }) + } + __nextHasNoMarginBottom + /> + + + value !== DEFAULT_VALUE} + onDeselect={() => + setAttributes({ value: DEFAULT_VALUE }) + } + isShownByDefault + > + + setAttributes({ value: newValue }) + } + help={__( + 'The value submitted when checkbox is checked', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: newValue }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    +
    + + +
    + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-checkbox-field/editor.scss b/src/blocks/form-checkbox-field/editor.scss new file mode 100644 index 0000000..3f9192a --- /dev/null +++ b/src/blocks/form-checkbox-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form Checkbox Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-checkbox-field/index.js b/src/blocks/form-checkbox-field/index.js new file mode 100644 index 0000000..010d410 --- /dev/null +++ b/src/blocks/form-checkbox-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Checkbox Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { check as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-checkbox-field/render.php b/src/blocks/form-checkbox-field/render.php new file mode 100644 index 0000000..682baa6 --- /dev/null +++ b/src/blocks/form-checkbox-field/render.php @@ -0,0 +1,74 @@ +'; + + $label_html = ''; + + $inner = '
    ' . $input . $label_html . '
    '; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--checkbox', + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_checkbox_field( $attributes, $content, $block ); diff --git a/src/blocks/form-checkbox-field/save.js b/src/blocks/form-checkbox-field/save.js new file mode 100644 index 0000000..5dcba37 --- /dev/null +++ b/src/blocks/form-checkbox-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Checkbox Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-checkbox-field/style.scss b/src/blocks/form-checkbox-field/style.scss new file mode 100644 index 0000000..2f80af2 --- /dev/null +++ b/src/blocks/form-checkbox-field/style.scss @@ -0,0 +1,71 @@ +/** + * Form Checkbox Field Block - Styles + * + * @since 1.0.0 + */ + +.airo-wp-form-field--checkbox { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + + .airo-wp-form-field__checkbox-wrapper { + display: flex; + align-items: flex-start; + gap: 0.75rem; + } + + .airo-wp-form-field__checkbox-input { + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; + margin-top: 0.125rem; + border: 1px solid var(--airo-wp-field-border-color, #d1d5db); + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + border-color: var(--airo-wp-field-focus-color, #2563eb); + } + + &:focus { + outline: 2px solid var(--airo-wp-field-focus-color, #2563eb); + outline-offset: 2px; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:checked { + background-color: var(--airo-wp-field-focus-color, #2563eb); + border-color: var(--airo-wp-field-focus-color, #2563eb); + } + } + + .airo-wp-form-field__checkbox-label { + flex: 1; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: var(--airo-wp-field-label-color, inherit); + cursor: pointer; + user-select: none; + } + + .airo-wp-form-field__required { + color: var(--airo-wp-field-error-color, #dc2626); + font-weight: 600; + } + + .airo-wp-form-field__help { + font-size: 0.875rem; + line-height: 1.5; + color: #6b7280; + margin: 0; + padding-left: 2rem; + } +} diff --git a/src/blocks/form-date-field/block.json b/src/blocks/form-date-field/block.json new file mode 100644 index 0000000..28086ff --- /dev/null +++ b/src/blocks/form-date-field/block.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-date-field", + "version": "1.0.0", + "title": "Date Field", + "category": "airo-wp", + "description": "A date picker input field for forms.", + "keywords": [ + "form", + "date", + "calendar", + "datepicker", + "day" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "calendar-alt", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "usesContext": [ + "airo-wp/form/fieldSpacing", + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/inputHeight", + "airo-wp/form/inputPadding" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Date" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "minDate": { + "type": "string", + "default": "" + }, + "maxDate": { + "type": "string", + "default": "" + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-date-field/deprecated.js b/src/blocks/form-date-field/deprecated.js new file mode 100644 index 0000000..49484bc --- /dev/null +++ b/src/blocks/form-date-field/deprecated.js @@ -0,0 +1,133 @@ +/** + * Form Date Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static date + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.5.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports. + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attribute schema for the static deprecation (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Date' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + minDate: { type: 'string', default: '' }, + maxDate: { type: 'string', default: '' }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static date field carries this wrapper class; the dynamic + // block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--date') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + helpText, + required, + defaultValue, + minDate, + maxDate, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--date' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic]; diff --git a/src/blocks/form-date-field/edit.js b/src/blocks/form-date-field/edit.js new file mode 100644 index 0000000..952861b --- /dev/null +++ b/src/blocks/form-date-field/edit.js @@ -0,0 +1,304 @@ +/** + * Form Date Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + ToggleControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormDateFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + helpText, + required, + defaultValue, + minDate, + maxDate, + fieldWidth, + } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `date-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames('airo-wp-form-field', 'airo-wp-form-field--date'); + + const fieldStyles = { + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Date', + helpText: '', + required: false, + defaultValue: '', + minDate: '', + maxDate: '', + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^date-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Date'} + onDeselect={() => setAttributes({ label: 'Date' })} + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + minDate !== ''} + onDeselect={() => setAttributes({ minDate: '' })} + isShownByDefault + > + + setAttributes({ minDate: value }) + } + help={__( + 'Format: YYYY–MM–DD (e.g., 2024–01–01)', + 'airo-wp' + )} + type="date" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + maxDate !== ''} + onDeselect={() => setAttributes({ maxDate: '' })} + isShownByDefault + > + + setAttributes({ maxDate: value }) + } + help={__( + 'Format: YYYY–MM–DD (e.g., 2024–12–31)', + 'airo-wp' + )} + type="date" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-filled date (Format: YYYY-MM-DD)', + 'airo-wp' + )} + type="date" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-date-field/editor.scss b/src/blocks/form-date-field/editor.scss new file mode 100644 index 0000000..c61d1e6 --- /dev/null +++ b/src/blocks/form-date-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form Date Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-date-field/index.js b/src/blocks/form-date-field/index.js new file mode 100644 index 0000000..aa61be7 --- /dev/null +++ b/src/blocks/form-date-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Date Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { calendar as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-date-field/render.php b/src/blocks/form-date-field/render.php new file mode 100644 index 0000000..cbdc6ef --- /dev/null +++ b/src/blocks/form-date-field/render.php @@ -0,0 +1,77 @@ +'; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $input; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--date', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_date_field( $attributes, $content, $block ); diff --git a/src/blocks/form-date-field/save.js b/src/blocks/form-date-field/save.js new file mode 100644 index 0000000..58f9ab9 --- /dev/null +++ b/src/blocks/form-date-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Date Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-date-field/style.scss b/src/blocks/form-date-field/style.scss new file mode 100644 index 0000000..740641d --- /dev/null +++ b/src/blocks/form-date-field/style.scss @@ -0,0 +1,10 @@ +/** + * Form Date Field Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// Date field uses the same styles as text field +@use '../form-text-field/style' as *; diff --git a/src/blocks/form-email-field/block.json b/src/blocks/form-email-field/block.json new file mode 100644 index 0000000..060f9fa --- /dev/null +++ b/src/blocks/form-email-field/block.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-email-field", + "version": "1.0.0", + "title": "Email Field", + "category": "airo-wp", + "description": "Email input field with built-in validation.", + "keywords": [ + "form", + "email", + "input", + "field" + ], + "textdomain": "airo-wp", + "icon": "email-alt", + "parent": [ + "airo-wp/form-builder" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Email" + }, + "placeholder": { + "type": "string", + "default": "" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "usesContext": [ + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/fieldSpacing", + "airo-wp/form/formId" + ], + "supports": { + "html": false, + "reusable": false, + "inserter": true + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-email-field/deprecated.js b/src/blocks/form-email-field/deprecated.js new file mode 100644 index 0000000..915df43 --- /dev/null +++ b/src/blocks/form-email-field/deprecated.js @@ -0,0 +1,250 @@ +/** + * Form Email Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static email + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.0.34 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports (with border → __experimentalBorder if applicable). + */ +const sharedSupports = { + html: false, + reusable: false, + inserter: true, +}; + +/** + * Shared attribute schema for the static deprecations (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Email' }, + placeholder: { type: 'string', default: '' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static email field carries this wrapper class; the dynamic + // block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--email') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--email' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +/** + * Version 1: Before aria-required was added to required fields. + * + * The site-designer-api generates HTML without aria-required="true" on + * required input fields. This deprecation matches that older format. + */ +const v1 = { + supports: sharedSupports, + attributes: { + fieldName: { + type: 'string', + default: '', + }, + label: { + type: 'string', + default: 'Email', + }, + placeholder: { + type: 'string', + default: '', + }, + helpText: { + type: 'string', + default: '', + }, + required: { + type: 'boolean', + default: false, + }, + defaultValue: { + type: 'string', + default: '', + }, + fieldWidth: { + type: 'string', + default: '100', + }, + }, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v1 blocks lack aria-required on input + return ( + innerHTML && + innerHTML.includes('type="email"') && + !innerHTML.includes('aria-required') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--email' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic, v1]; diff --git a/src/blocks/form-email-field/edit.js b/src/blocks/form-email-field/edit.js new file mode 100644 index 0000000..68b9e25 --- /dev/null +++ b/src/blocks/form-email-field/edit.js @@ -0,0 +1,267 @@ +/** + * Form Email Field Block - Editor Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + TextareaControl, + ToggleControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormEmailFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + fieldWidth, + } = attributes; + + // Generate unique field name on mount if not set + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `field_${clientId.substring(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldLabelColor = context['airo-wp/form/fieldLabelColor']; + const fieldBorderColor = context['airo-wp/form/fieldBorderColor']; + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--email' + ); + + const fieldStyles = { + '--airo-wp-field-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-field-border-color': convertColorToCSSVar(fieldBorderColor), + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Email', + placeholder: '', + helpText: '', + required: false, + defaultValue: '', + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^field_[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_]/g, + '_' + ), + }) + } + help={__( + 'Unique identifier for this field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Email'} + onDeselect={() => setAttributes({ label: 'Email' })} + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + placeholder !== ''} + onDeselect={() => setAttributes({ placeholder: '' })} + isShownByDefault + > + + setAttributes({ placeholder: value }) + } + placeholder="email@example.com" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + rows={2} + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + type="email" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + + + + {helpText && ( +

    {helpText}

    + )} +
    + + ); +} diff --git a/src/blocks/form-email-field/editor.scss b/src/blocks/form-email-field/editor.scss new file mode 100644 index 0000000..b4790ec --- /dev/null +++ b/src/blocks/form-email-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form Email Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-email-field/index.js b/src/blocks/form-email-field/index.js new file mode 100644 index 0000000..1bc5ec7 --- /dev/null +++ b/src/blocks/form-email-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Email Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { atSymbol as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-email-field/render.php b/src/blocks/form-email-field/render.php new file mode 100644 index 0000000..7fcfb5e --- /dev/null +++ b/src/blocks/form-email-field/render.php @@ -0,0 +1,73 @@ +'; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $input; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--email', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_email_field( $attributes, $content, $block ); diff --git a/src/blocks/form-email-field/save.js b/src/blocks/form-email-field/save.js new file mode 100644 index 0000000..a0448b8 --- /dev/null +++ b/src/blocks/form-email-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Email Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-email-field/style.scss b/src/blocks/form-email-field/style.scss new file mode 100644 index 0000000..ecccbba --- /dev/null +++ b/src/blocks/form-email-field/style.scss @@ -0,0 +1,10 @@ +/** + * Form Email Field Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// Email field uses the same styles as text field +@use '../form-text-field/style' as *; diff --git a/src/blocks/form-hidden-field/block.json b/src/blocks/form-hidden-field/block.json new file mode 100644 index 0000000..b89e355 --- /dev/null +++ b/src/blocks/form-hidden-field/block.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-hidden-field", + "version": "1.0.0", + "title": "Hidden Field", + "category": "airo-wp", + "description": "A hidden field for passing data in forms.", + "keywords": [ + "form", + "hidden", + "data", + "value" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "hidden", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "value": { + "type": "string", + "default": "" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-hidden-field/deprecated.js b/src/blocks/form-hidden-field/deprecated.js new file mode 100644 index 0000000..44d1319 --- /dev/null +++ b/src/blocks/form-hidden-field/deprecated.js @@ -0,0 +1,74 @@ +/** + * Form Hidden Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static hidden + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.5.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports. + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attribute schema for the static deprecation (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + value: { type: 'string', default: '' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static hidden field carries this wrapper class; the + // dynamic block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--hidden') + ); + }, + + save({ attributes }) { + const { fieldName, value } = attributes; + + const blockProps = useBlockProps.save({ + className: 'airo-wp-form-field airo-wp-form-field--hidden', + }); + + return ( +
    + +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic]; diff --git a/src/blocks/form-hidden-field/edit.js b/src/blocks/form-hidden-field/edit.js new file mode 100644 index 0000000..ddda9c3 --- /dev/null +++ b/src/blocks/form-hidden-field/edit.js @@ -0,0 +1,105 @@ +/** + * Form Hidden Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { TextControl, Notice } from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; + +export default function FormHiddenFieldEdit({ + attributes, + setAttributes, + clientId, +}) { + const { fieldName, value } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `hidden-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + const blockProps = useBlockProps({ + className: 'airo-wp-form-field airo-wp-form-field--hidden', + }); + + return ( + <> + + + setAttributes({ + fieldName: '', + value: '', + }) + } + > + + !!fieldName && + !/^hidden-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: newValue.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + value !== ''} + onDeselect={() => setAttributes({ value: '' })} + isShownByDefault + > + + setAttributes({ value: newValue }) + } + help={__( + 'The hidden value to be submitted with the form', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + {__('Hidden Field:', 'airo-wp')}{' '} + {fieldName} = {value || __('(empty)', 'airo-wp')} + +
    + + ); +} diff --git a/src/blocks/form-hidden-field/editor.scss b/src/blocks/form-hidden-field/editor.scss new file mode 100644 index 0000000..2e527be --- /dev/null +++ b/src/blocks/form-hidden-field/editor.scss @@ -0,0 +1,14 @@ +/** + * Form Hidden Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; + +// Show in editor with visual indicator +.airo-wp-form-field--hidden { + display: block; // Override frontend hiding + margin-bottom: 1rem; +} diff --git a/src/blocks/form-hidden-field/index.js b/src/blocks/form-hidden-field/index.js new file mode 100644 index 0000000..5a6b684 --- /dev/null +++ b/src/blocks/form-hidden-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Hidden Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { keyboardClose as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-hidden-field/render.php b/src/blocks/form-hidden-field/render.php new file mode 100644 index 0000000..5ce0421 --- /dev/null +++ b/src/blocks/form-hidden-field/render.php @@ -0,0 +1,44 @@ +'; + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--hidden', + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_hidden_field( $attributes, $content, $block ); diff --git a/src/blocks/form-hidden-field/save.js b/src/blocks/form-hidden-field/save.js new file mode 100644 index 0000000..34a0e85 --- /dev/null +++ b/src/blocks/form-hidden-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Hidden Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and this field's + * markup can never trip block validation. Historical static markup is + * migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-hidden-field/style.scss b/src/blocks/form-hidden-field/style.scss new file mode 100644 index 0000000..bc0d016 --- /dev/null +++ b/src/blocks/form-hidden-field/style.scss @@ -0,0 +1,9 @@ +/** + * Form Hidden Field Block - Styles + * + * @since 1.0.0 + */ + +.airo-wp-form-field--hidden { + display: none; +} diff --git a/src/blocks/form-number-field/block.json b/src/blocks/form-number-field/block.json new file mode 100644 index 0000000..a523b0a --- /dev/null +++ b/src/blocks/form-number-field/block.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-number-field", + "version": "1.0.0", + "title": "Number Field", + "category": "airo-wp", + "description": "A number input field for forms.", + "keywords": [ + "form", + "number", + "input", + "integer", + "decimal" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "calculator", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "usesContext": [ + "airo-wp/form/fieldSpacing", + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/inputHeight", + "airo-wp/form/inputPadding" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Number" + }, + "placeholder": { + "type": "string", + "default": "" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "min": { + "type": "number", + "default": null + }, + "max": { + "type": "number", + "default": null + }, + "step": { + "type": "number", + "default": 1 + }, + "allowDecimals": { + "type": "boolean", + "default": false + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-number-field/deprecated.js b/src/blocks/form-number-field/deprecated.js new file mode 100644 index 0000000..5925649 --- /dev/null +++ b/src/blocks/form-number-field/deprecated.js @@ -0,0 +1,141 @@ +/** + * Form Number Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static number + * field so existing content migrates silently (passthrough) with no "Attempt + * Recovery" warning. + * + * @since 2.5.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports. + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attribute schema for the static deprecation (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Number' }, + placeholder: { type: 'string', default: '' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + min: { type: 'number', default: null }, + max: { type: 'number', default: null }, + step: { type: 'number', default: 1 }, + allowDecimals: { type: 'boolean', default: false }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static number field carries this wrapper class; the dynamic + // block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--number') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + min, + max, + step, + allowDecimals, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--number' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic]; diff --git a/src/blocks/form-number-field/edit.js b/src/blocks/form-number-field/edit.js new file mode 100644 index 0000000..a0902b7 --- /dev/null +++ b/src/blocks/form-number-field/edit.js @@ -0,0 +1,389 @@ +/** + * Form Number Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + ToggleControl, + SelectControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- NumberControl is stable in practice + __experimentalNumberControl as NumberControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormNumberFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + min, + max, + step, + allowDecimals, + fieldWidth, + } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `number-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--number' + ); + + const fieldStyles = { + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Number', + placeholder: '', + helpText: '', + required: false, + defaultValue: '', + min: null, + max: null, + step: 1, + allowDecimals: false, + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^number-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Number'} + onDeselect={() => setAttributes({ label: 'Number' })} + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + min !== null} + onDeselect={() => setAttributes({ min: null })} + isShownByDefault + > + + setAttributes({ + min: + value !== '' ? parseFloat(value) : null, + }) + } + help={__( + 'Minimum allowed value (leave empty for no minimum)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + max !== null} + onDeselect={() => setAttributes({ max: null })} + isShownByDefault + > + + setAttributes({ + max: + value !== '' ? parseFloat(value) : null, + }) + } + help={__( + 'Maximum allowed value (leave empty for no maximum)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + allowDecimals !== false} + onDeselect={() => + setAttributes({ allowDecimals: false, step: 1 }) + } + isShownByDefault + > + { + setAttributes({ allowDecimals: value }); + if (!value && step < 1) { + setAttributes({ step: 1 }); + } + }} + help={__( + 'Allow decimal numbers instead of integers only', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + step !== 1} + onDeselect={() => setAttributes({ step: 1 })} + isShownByDefault + > + + setAttributes({ step: parseFloat(value) || 1 }) + } + help={__( + 'Increment/decrement step value', + 'airo-wp' + )} + min={0.01} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + placeholder !== ''} + onDeselect={() => setAttributes({ placeholder: '' })} + isShownByDefault + > + + setAttributes({ placeholder: value }) + } + help={__( + 'Example text shown when field is empty', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-filled value for this field', + 'airo-wp' + )} + type="number" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-number-field/editor.scss b/src/blocks/form-number-field/editor.scss new file mode 100644 index 0000000..e5a29fb --- /dev/null +++ b/src/blocks/form-number-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form Number Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-number-field/index.js b/src/blocks/form-number-field/index.js new file mode 100644 index 0000000..6237254 --- /dev/null +++ b/src/blocks/form-number-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Number Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { plusCircle as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-number-field/render.php b/src/blocks/form-number-field/render.php new file mode 100644 index 0000000..4434769 --- /dev/null +++ b/src/blocks/form-number-field/render.php @@ -0,0 +1,85 @@ +'; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $input; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--number', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_number_field( $attributes, $content, $block ); diff --git a/src/blocks/form-number-field/save.js b/src/blocks/form-number-field/save.js new file mode 100644 index 0000000..2f2e05f --- /dev/null +++ b/src/blocks/form-number-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Number Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-number-field/style.scss b/src/blocks/form-number-field/style.scss new file mode 100644 index 0000000..d8a29cc --- /dev/null +++ b/src/blocks/form-number-field/style.scss @@ -0,0 +1,26 @@ +/** + * Form Number Field Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// Number field uses the same styles as text field +@use '../form-text-field/style' as *; + +// Number-specific adjustments +.airo-wp-form-field--number { + + input[type="number"] { + // Remove spinner arrows in WebKit browsers + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + // For Firefox + -moz-appearance: textfield; + } +} diff --git a/src/blocks/form-phone-field/block.json b/src/blocks/form-phone-field/block.json new file mode 100644 index 0000000..2fa6d33 --- /dev/null +++ b/src/blocks/form-phone-field/block.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-phone-field", + "version": "1.0.0", + "title": "Phone Field", + "category": "airo-wp", + "description": "A phone number input field for forms.", + "keywords": [ + "form", + "phone", + "telephone", + "tel", + "mobile" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "phone", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "usesContext": [ + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/fieldSpacing", + "airo-wp/form/formId" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Phone Number" + }, + "placeholder": { + "type": "string", + "default": "" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "phoneFormat": { + "type": "string", + "default": "any", + "enum": [ + "any", + "us", + "international" + ] + }, + "showCountryCode": { + "type": "boolean", + "default": true + }, + "countryCode": { + "type": "string", + "default": "+1" + }, + "autoFormat": { + "type": "boolean", + "default": true + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "editorScript": "file:./index.js", + "viewScript": "file:./view.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-phone-field/country-codes.js b/src/blocks/form-phone-field/country-codes.js new file mode 100644 index 0000000..dee9335 --- /dev/null +++ b/src/blocks/form-phone-field/country-codes.js @@ -0,0 +1,69 @@ +/** + * Country calling codes for the phone field. + * + * Shared between edit.js (editor dropdown), save.js (static markup), + * and view.js (frontend hydration). + * + * Sorted numerically by calling code. + * + * @since 2.0.32 + */ + +const COUNTRY_CODES = [ + { value: '+1', label: '+1 (US/Canada)' }, + { value: '+7', label: '+7 (Russia)' }, + { value: '+20', label: '+20 (Egypt)' }, + { value: '+27', label: '+27 (South Africa)' }, + { value: '+30', label: '+30 (Greece)' }, + { value: '+31', label: '+31 (Netherlands)' }, + { value: '+32', label: '+32 (Belgium)' }, + { value: '+33', label: '+33 (France)' }, + { value: '+34', label: '+34 (Spain)' }, + { value: '+36', label: '+36 (Hungary)' }, + { value: '+39', label: '+39 (Italy)' }, + { value: '+40', label: '+40 (Romania)' }, + { value: '+41', label: '+41 (Switzerland)' }, + { value: '+43', label: '+43 (Austria)' }, + { value: '+44', label: '+44 (UK)' }, + { value: '+45', label: '+45 (Denmark)' }, + { value: '+46', label: '+46 (Sweden)' }, + { value: '+47', label: '+47 (Norway)' }, + { value: '+48', label: '+48 (Poland)' }, + { value: '+49', label: '+49 (Germany)' }, + { value: '+51', label: '+51 (Peru)' }, + { value: '+52', label: '+52 (Mexico)' }, + { value: '+54', label: '+54 (Argentina)' }, + { value: '+55', label: '+55 (Brazil)' }, + { value: '+56', label: '+56 (Chile)' }, + { value: '+57', label: '+57 (Colombia)' }, + { value: '+60', label: '+60 (Malaysia)' }, + { value: '+61', label: '+61 (Australia)' }, + { value: '+62', label: '+62 (Indonesia)' }, + { value: '+63', label: '+63 (Philippines)' }, + { value: '+64', label: '+64 (New Zealand)' }, + { value: '+65', label: '+65 (Singapore)' }, + { value: '+66', label: '+66 (Thailand)' }, + { value: '+81', label: '+81 (Japan)' }, + { value: '+82', label: '+82 (South Korea)' }, + { value: '+84', label: '+84 (Vietnam)' }, + { value: '+86', label: '+86 (China)' }, + { value: '+90', label: '+90 (Turkey)' }, + { value: '+91', label: '+91 (India)' }, + { value: '+92', label: '+92 (Pakistan)' }, + { value: '+233', label: '+233 (Ghana)' }, + { value: '+234', label: '+234 (Nigeria)' }, + { value: '+254', label: '+254 (Kenya)' }, + { value: '+351', label: '+351 (Portugal)' }, + { value: '+353', label: '+353 (Ireland)' }, + { value: '+358', label: '+358 (Finland)' }, + { value: '+380', label: '+380 (Ukraine)' }, + { value: '+420', label: '+420 (Czech Republic)' }, + { value: '+852', label: '+852 (Hong Kong)' }, + { value: '+880', label: '+880 (Bangladesh)' }, + { value: '+886', label: '+886 (Taiwan)' }, + { value: '+966', label: '+966 (Saudi Arabia)' }, + { value: '+971', label: '+971 (UAE)' }, + { value: '+972', label: '+972 (Israel)' }, +]; + +export default COUNTRY_CODES; diff --git a/src/blocks/form-phone-field/deprecated.js b/src/blocks/form-phone-field/deprecated.js new file mode 100644 index 0000000..4f36e51 --- /dev/null +++ b/src/blocks/form-phone-field/deprecated.js @@ -0,0 +1,764 @@ +/** + * Form Phone Field Block - Deprecated Versions + * + * Newest → oldest. WordPress tries each in order until one matches. + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static phone + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.0.32 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports (with border → __experimentalBorder if applicable). + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attributes definition for all deprecated versions. + * All versions share the same attribute schema. + */ +const sharedAttributes = { + fieldName: { + type: 'string', + default: '', + }, + label: { + type: 'string', + default: 'Phone Number', + }, + placeholder: { + type: 'string', + default: '', + }, + helpText: { + type: 'string', + default: '', + }, + required: { + type: 'boolean', + default: false, + }, + defaultValue: { + type: 'string', + default: '', + }, + phoneFormat: { + type: 'string', + default: 'any', + enum: ['any', 'us', 'international'], + }, + showCountryCode: { + type: 'boolean', + default: true, + }, + countryCode: { + type: 'string', + default: '+1', + }, + autoFormat: { + type: 'boolean', + default: true, + }, + fieldWidth: { + type: 'string', + default: '100', + }, +}; + +/** + * Shared helper: build blockProps for save. + * @param {string} fieldWidth + */ +function getSaveBlockProps(fieldWidth) { + return useBlockProps.save({ + className: classnames('airo-wp-form-field', 'airo-wp-form-field--phone'), + style: { + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); +} + +/** + * Shared helper: get phone pattern. + * @param {string} phoneFormat + */ +function getPattern(phoneFormat) { + switch (phoneFormat) { + case 'us': + return '[0-9]{3}-[0-9]{3}-[0-9]{4}'; + case 'international': + return '\\+[0-9]{1,3}[0-9\\s\\-]{4,14}'; + default: + return undefined; + } +} + +/** + * Shared helper: get placeholder. + * @param {string} placeholder + * @param {string} phoneFormat + */ +function getPlaceholderText(placeholder, phoneFormat) { + if (placeholder) { + return placeholder; + } + switch (phoneFormat) { + case 'us': + return '555-123-4567'; + case 'international': + return '+1 555 123 4567'; + default: + return ''; + } +} + +/** + * vStatic: the last static markup, immediately before the block became + * server-rendered. Identical output to v4's save(), but matches ANY current- + * format phone field (data-airo-wp-country-code, empty at runtime from the shared COUNTRY_CODES constant. + // The form already requires JS for auto-formatting, so the + // no-JS experience is an accepted trade-off. + +
    + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +/** + * Version 4: React serializes numeric `flex: 1` as `flex:1px` (it treats + * numeric style values for non-unitless properties as pixel values). The + * input's style was `style={{ flex: 1 }}` in save.js, causing WP's block + * serializer to emit `style="flex:1px"`. This deprecation covers that legacy + * era so existing blocks with `flex:1px` silently migrate to the fixed + * `flex:1` output produced by the corrected save.js. + * + * Distinguishing signature: `data-airo-wp-country-code` present (current-format + * empty (data-airo-wp-country-code, no + // + + + + + + + + + + + + + + )} + +
    + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +/** + * Version 2: Used `selected` attribute on individual + + + + + + + + + + + + + + )} + +
    + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic, v4, v3, v2, v1]; diff --git a/src/blocks/form-phone-field/edit.js b/src/blocks/form-phone-field/edit.js new file mode 100644 index 0000000..e6c9016 --- /dev/null +++ b/src/blocks/form-phone-field/edit.js @@ -0,0 +1,461 @@ +/** + * Form Phone Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + ToggleControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; +import COUNTRY_CODES from './country-codes'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormPhoneFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + phoneFormat, + showCountryCode, + countryCode, + autoFormat, + fieldWidth, + } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `phone-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldLabelColor = context['airo-wp/form/fieldLabelColor']; + const fieldBorderColor = context['airo-wp/form/fieldBorderColor']; + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--phone' + ); + + const fieldStyles = { + '--airo-wp-field-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-field-border-color': convertColorToCSSVar(fieldBorderColor), + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + // Get pattern based on phone format + const getPattern = () => { + switch (phoneFormat) { + case 'us': + return '[0-9]{3}-[0-9]{3}-[0-9]{4}'; + case 'international': + return '\\+[0-9]{1,3}[0-9\\s\\-]{4,14}'; + default: + return undefined; + } + }; + + // Get placeholder based on format + const getPlaceholder = () => { + if (placeholder) { + return placeholder; + } + + switch (phoneFormat) { + case 'us': + return '555-123-4567'; + case 'international': + return '+1 555 123 4567'; + default: + return ''; + } + }; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Phone Number', + placeholder: '', + helpText: '', + required: false, + defaultValue: '', + phoneFormat: 'any', + showCountryCode: true, + countryCode: '+1', + autoFormat: true, + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^phone-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Phone Number'} + onDeselect={() => + setAttributes({ label: 'Phone Number' }) + } + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + phoneFormat !== 'any'} + onDeselect={() => setAttributes({ phoneFormat: 'any' })} + isShownByDefault + > + + setAttributes({ phoneFormat: value }) + } + help={__( + 'Choose how phone numbers should be formatted', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + showCountryCode !== true} + onDeselect={() => + setAttributes({ showCountryCode: true }) + } + isShownByDefault + > + + setAttributes({ showCountryCode: value }) + } + help={__( + 'Display a dropdown for selecting country code', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + {showCountryCode && ( + countryCode !== '+1'} + onDeselect={() => + setAttributes({ countryCode: '+1' }) + } + isShownByDefault + > + + setAttributes({ countryCode: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + autoFormat !== true} + onDeselect={() => setAttributes({ autoFormat: true })} + isShownByDefault + > + + setAttributes({ autoFormat: value }) + } + help={__( + 'Automatically format phone number as user types', + 'airo-wp' + )} + __nextHasNoMarginBottom + /> + + + placeholder !== ''} + onDeselect={() => setAttributes({ placeholder: '' })} + isShownByDefault + > + + setAttributes({ placeholder: value }) + } + help={__( + 'Example text shown when field is empty', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-filled value for this field', + 'airo-wp' + )} + type="tel" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + +
    + {showCountryCode && ( + + )} + +
    + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-phone-field/editor.scss b/src/blocks/form-phone-field/editor.scss new file mode 100644 index 0000000..7f41a74 --- /dev/null +++ b/src/blocks/form-phone-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form Phone Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-phone-field/index.js b/src/blocks/form-phone-field/index.js new file mode 100644 index 0000000..e819475 --- /dev/null +++ b/src/blocks/form-phone-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Phone Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { inbox as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-phone-field/render.php b/src/blocks/form-phone-field/render.php new file mode 100644 index 0000000..cdf7af0 --- /dev/null +++ b/src/blocks/form-phone-field/render.php @@ -0,0 +1,123 @@ + is intentionally emitted empty: view.js hydrates its + * } + {options + .filter( + (option) => !(placeholder && option.value === '') + ) + .map((option, index) => ( + + ))} + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +/** + * Version 2: Before aria-required was added to required fields. + * + * The site-designer-api generates HTML without aria-required="true" on + * required select fields. This deprecation matches that older format. + */ +const v2 = { + supports: sharedSupports, + attributes: { + fieldName: { + type: 'string', + default: '', + }, + label: { + type: 'string', + default: 'Select Option', + }, + helpText: { + type: 'string', + default: '', + }, + required: { + type: 'boolean', + default: false, + }, + options: { + type: 'array', + default: [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + { label: 'Option 3', value: 'option-3' }, + ], + items: { + type: 'object', + properties: { + label: { type: 'string' }, + value: { type: 'string' }, + }, + }, + }, + placeholder: { + type: 'string', + default: '-- Select an option --', + }, + fieldWidth: { + type: 'string', + default: '100', + }, + }, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v2 blocks lack aria-required on select + return ( + innerHTML && + innerHTML.includes('airo-wp-form-field__select') && + !innerHTML.includes('aria-required') + ); + }, + migrate(attributes) { + return attributes; + }, + + save({ attributes }) { + const { + fieldName, + label, + helpText, + required, + options, + placeholder, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--select' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, +}; + +/** + * Version 1: Original version with defaultValue on + {placeholder && } + {options.map((option, index) => ( + + ))} + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic, v2, v1]; diff --git a/src/blocks/form-select-field/edit.js b/src/blocks/form-select-field/edit.js new file mode 100644 index 0000000..d0e2232 --- /dev/null +++ b/src/blocks/form-select-field/edit.js @@ -0,0 +1,398 @@ +/** + * Form Select Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + ToggleControl, + SelectControl, + Button, + Flex, + FlexItem, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +const DEFAULT_OPTIONS = [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + { label: 'Option 3', value: 'option-3' }, +]; +const DEFAULT_PLACEHOLDER = '-- Select an option --'; + +// Deep-equality check against DEFAULT_OPTIONS. Options is an array of +// {label, value} pairs; matches the block.json default exactly when the +// author hasn't touched the Options list. +const isDefaultOptions = (opts) => + Array.isArray(opts) && + opts.length === DEFAULT_OPTIONS.length && + opts.every( + (opt, i) => + opt?.label === DEFAULT_OPTIONS[i].label && + opt?.value === DEFAULT_OPTIONS[i].value + ); + +export default function FormSelectFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + helpText, + required, + defaultValue, + options, + placeholder, + fieldWidth, + } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `select-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--select' + ); + + const fieldStyles = { + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + // Add new option + const addOption = () => { + const newOptions = [ + ...options, + { + label: `Option ${options.length + 1}`, + value: `option-${options.length + 1}`, + }, + ]; + setAttributes({ options: newOptions }); + }; + + // Remove option + const removeOption = (index) => { + const newOptions = options.filter((_, i) => i !== index); + setAttributes({ options: newOptions }); + }; + + // Update option + const updateOption = (index, field, value) => { + const newOptions = options.map((option, i) => { + if (i === index) { + return { ...option, [field]: value }; + } + return option; + }); + setAttributes({ options: newOptions }); + }; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Select Option', + helpText: '', + required: false, + defaultValue: '', + options: DEFAULT_OPTIONS, + placeholder: DEFAULT_PLACEHOLDER, + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^select-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Select Option'} + onDeselect={() => + setAttributes({ label: 'Select Option' }) + } + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + !isDefaultOptions(options)} + onDeselect={() => + setAttributes({ options: DEFAULT_OPTIONS }) + } + isShownByDefault + > + {options.map((option, index) => ( + + + + updateOption(index, 'label', value) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + updateOption(index, 'value', value) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + + + placeholder !== DEFAULT_PLACEHOLDER} + onDeselect={() => + setAttributes({ placeholder: DEFAULT_PLACEHOLDER }) + } + isShownByDefault + > + + setAttributes({ placeholder: value }) + } + help={__( + 'Text shown when no option is selected', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-selected option value', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-select-field/editor.scss b/src/blocks/form-select-field/editor.scss new file mode 100644 index 0000000..b329066 --- /dev/null +++ b/src/blocks/form-select-field/editor.scss @@ -0,0 +1,19 @@ +/** + * Form Select Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; + +// Editor-specific: WordPress editor styles override select width +.editor-styles-wrapper { + + .airo-wp-form-field--select { + + .airo-wp-form-field__select { + width: 100%; + } + } +} diff --git a/src/blocks/form-select-field/index.js b/src/blocks/form-select-field/index.js new file mode 100644 index 0000000..1c1bff3 --- /dev/null +++ b/src/blocks/form-select-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Select Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { menu as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-select-field/render.php b/src/blocks/form-select-field/render.php new file mode 100644 index 0000000..50e3596 --- /dev/null +++ b/src/blocks/form-select-field/render.php @@ -0,0 +1,101 @@ +'; + + if ( '' !== $placeholder ) { + $select .= ''; + } + + foreach ( $options as $option ) { + $option_value = isset( $option['value'] ) ? (string) $option['value'] : ''; + $option_label = isset( $option['label'] ) ? (string) $option['label'] : ''; + + // Mirror save.js: when a placeholder is present, skip any option that + // duplicates the empty placeholder value. + if ( '' !== $placeholder && '' === $option_value ) { + continue; + } + + // Honour the editor's "Default Value" control by pre-selecting the matching + // option. The removed static save() never did this, so a select where an + // author set a Default Value now reflects it on the frontend — an intentional + // fix of a control that was previously a silent no-op. + $selected = ( '' !== $default_val && $option_value === $default_val ) ? ' selected' : ''; + + $select .= ''; + } + + $select .= ''; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $select; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--select', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_select_field( $attributes, $content, $block ); diff --git a/src/blocks/form-select-field/save.js b/src/blocks/form-select-field/save.js new file mode 100644 index 0000000..563970c --- /dev/null +++ b/src/blocks/form-select-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Select Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-select-field/style.scss b/src/blocks/form-select-field/style.scss new file mode 100644 index 0000000..07c3f67 --- /dev/null +++ b/src/blocks/form-select-field/style.scss @@ -0,0 +1,24 @@ +/** + * Form Select Field Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// Select field uses the same styles as text field +@use '../form-text-field/style' as *; + +// Select-specific adjustments +.airo-wp-form-field--select { + + .airo-wp-form-field__select { + cursor: pointer; + appearance: none; + /* stylelint-disable-next-line function-url-quotes */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M10.293 3.293L6 7.586 1.707 3.293A1 1 0 00.293 4.707l5 5a1 1 0 001.414 0l5-5a1 1 0 10-1.414-1.414z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + padding-right: 2.5rem; + } +} diff --git a/src/blocks/form-text-field/block.json b/src/blocks/form-text-field/block.json new file mode 100644 index 0000000..a822f19 --- /dev/null +++ b/src/blocks/form-text-field/block.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-text-field", + "version": "1.0.0", + "title": "Text Field", + "category": "airo-wp", + "description": "Single-line text input field for forms.", + "keywords": [ + "form", + "input", + "text", + "field" + ], + "textdomain": "airo-wp", + "icon": "edit", + "parent": [ + "airo-wp/form-builder" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Text Field" + }, + "placeholder": { + "type": "string", + "default": "" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "minLength": { + "type": "number", + "default": 0 + }, + "maxLength": { + "type": "number", + "default": 0 + }, + "validation": { + "type": "string", + "default": "none", + "enum": [ + "none", + "letters", + "numbers", + "alphanumeric", + "custom" + ] + }, + "validationPattern": { + "type": "string", + "default": "" + }, + "validationMessage": { + "type": "string", + "default": "" + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "usesContext": [ + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/fieldSpacing", + "airo-wp/form/formId" + ], + "supports": { + "html": false, + "reusable": false, + "inserter": true + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-text-field/deprecated.js b/src/blocks/form-text-field/deprecated.js new file mode 100644 index 0000000..562812b --- /dev/null +++ b/src/blocks/form-text-field/deprecated.js @@ -0,0 +1,275 @@ +/** + * Form Text Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static text + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.0.34 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports (with border → __experimentalBorder if applicable). + */ +const sharedSupports = { + html: false, + reusable: false, + inserter: true, +}; + +/** + * Shared attribute schema for the static deprecations (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Text Field' }, + placeholder: { type: 'string', default: '' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + minLength: { type: 'number', default: 0 }, + maxLength: { type: 'number', default: 0 }, + validation: { type: 'string', default: 'none' }, + validationPattern: { type: 'string', default: '' }, + validationMessage: { type: 'string', default: '' }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static text field carries this wrapper class; the dynamic + // block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--text') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + minLength, + maxLength, + validation, + validationPattern, + validationMessage, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--text' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const getValidationPattern = () => { + switch (validation) { + case 'letters': + return '[A-Za-z\\s]+'; + case 'numbers': + return '[0-9]+'; + case 'alphanumeric': + return '[A-Za-z0-9]+'; + case 'custom': + return validationPattern; + default: + return null; + } + }; + + const pattern = getValidationPattern(); + const fieldId = `field-${fieldName}`; + + return ( +
    + + + 0 ? minLength : undefined} + maxLength={maxLength > 0 ? maxLength : undefined} + pattern={pattern || undefined} + title={validationMessage || undefined} + defaultValue={defaultValue || undefined} + aria-describedby={helpText ? `${fieldId}-help` : undefined} + aria-required={required ? 'true' : undefined} + data-field-type="text" + /> + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +/** + * Version 1: Before aria-required was added to required fields. + * + * The site-designer-api generated HTML without aria-required="true" on required + * input fields. Kept for provenance; vStatic already covers this content. + */ +const v1 = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // v1 blocks lack aria-required on input + return ( + innerHTML && + innerHTML.includes('type="text"') && + !innerHTML.includes('aria-required') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + minLength, + maxLength, + validation, + validationPattern, + validationMessage, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--text' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const getValidationPattern = () => { + switch (validation) { + case 'letters': + return '[A-Za-z\\s]+'; + case 'numbers': + return '[0-9]+'; + case 'alphanumeric': + return '[A-Za-z0-9]+'; + case 'custom': + return validationPattern; + default: + return null; + } + }; + + const pattern = getValidationPattern(); + const fieldId = `field-${fieldName}`; + + return ( +
    + + + 0 ? minLength : undefined} + maxLength={maxLength > 0 ? maxLength : undefined} + pattern={pattern || undefined} + title={validationMessage || undefined} + defaultValue={defaultValue || undefined} + aria-describedby={helpText ? `${fieldId}-help` : undefined} + data-field-type="text" + /> + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic, v1]; diff --git a/src/blocks/form-text-field/edit.js b/src/blocks/form-text-field/edit.js new file mode 100644 index 0000000..9dfd8da --- /dev/null +++ b/src/blocks/form-text-field/edit.js @@ -0,0 +1,432 @@ +/** + * Form Text Field Block - Editor Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + TextareaControl, + ToggleControl, + RangeControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormTextFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + minLength, + maxLength, + validation, + validationPattern, + validationMessage, + fieldWidth, + } = attributes; + + // Generate unique field name on mount if not set + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `field_${clientId.substring(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldLabelColor = context['airo-wp/form/fieldLabelColor']; + const fieldBorderColor = context['airo-wp/form/fieldBorderColor']; + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames('airo-wp-form-field', 'airo-wp-form-field--text'); + + const fieldStyles = { + '--airo-wp-field-label-color': convertColorToCSSVar(fieldLabelColor), + '--airo-wp-field-border-color': convertColorToCSSVar(fieldBorderColor), + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Text Field', + placeholder: '', + helpText: '', + required: false, + defaultValue: '', + minLength: 0, + maxLength: 0, + validation: 'none', + validationPattern: '', + validationMessage: '', + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^field_[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_]/g, + '_' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, underscores)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Text Field'} + onDeselect={() => + setAttributes({ label: 'Text Field' }) + } + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + placeholder !== ''} + onDeselect={() => setAttributes({ placeholder: '' })} + isShownByDefault + > + + setAttributes({ placeholder: value }) + } + help={__( + 'Hint text shown inside the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + rows={2} + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-fill this field with a default value', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + minLength !== 0} + onDeselect={() => setAttributes({ minLength: 0 })} + isShownByDefault + > + + setAttributes({ minLength: value }) + } + min={0} + max={500} + help={__( + 'Minimum number of characters required (0 = no minimum)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + maxLength !== 0} + onDeselect={() => setAttributes({ maxLength: 0 })} + isShownByDefault + > + + setAttributes({ maxLength: value }) + } + min={0} + max={500} + help={__( + 'Maximum number of characters allowed (0 = no maximum)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + validation !== 'none'} + onDeselect={() => + setAttributes({ + validation: 'none', + validationPattern: '', + validationMessage: '', + }) + } + isShownByDefault + > + + setAttributes({ validation: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + {validation === 'custom' && ( + validationPattern !== ''} + onDeselect={() => + setAttributes({ validationPattern: '' }) + } + isShownByDefault + > + + setAttributes({ validationPattern: value }) + } + help={__( + 'Regular expression for validation', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + {validation === 'custom' && ( + validationMessage !== ''} + onDeselect={() => + setAttributes({ validationMessage: '' }) + } + isShownByDefault + > + + setAttributes({ validationMessage: value }) + } + help={__( + 'Message shown when validation fails', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + )} + + + +
    + + + + + {helpText && ( +

    {helpText}

    + )} +
    + + ); +} diff --git a/src/blocks/form-text-field/editor.scss b/src/blocks/form-text-field/editor.scss new file mode 100644 index 0000000..b22385f --- /dev/null +++ b/src/blocks/form-text-field/editor.scss @@ -0,0 +1,23 @@ +/** + * Form Text Field Block - Editor Styles + * + * CRITICAL: Must be duplicated from style.scss for editor/frontend parity + * + * @since 1.0.0 + */ + +// Import the main frontend styles for base styling +@import './style'; + +// Editor-specific overrides +.editor-styles-wrapper { + + .airo-wp-form-field { + // Make fields look disabled in editor + &__input, + &__textarea, + &__select { + cursor: default; + } + } +} diff --git a/src/blocks/form-text-field/index.js b/src/blocks/form-text-field/index.js new file mode 100644 index 0000000..a1fff57 --- /dev/null +++ b/src/blocks/form-text-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Text Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { edit as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-text-field/render.php b/src/blocks/form-text-field/render.php new file mode 100644 index 0000000..6fea199 --- /dev/null +++ b/src/blocks/form-text-field/render.php @@ -0,0 +1,107 @@ + 0 ) { + $input .= ' minlength="' . $min_length . '"'; + } + if ( $max_length > 0 ) { + $input .= ' maxlength="' . $max_length . '"'; + } + if ( '' !== $pattern ) { + $input .= ' pattern="' . esc_attr( $pattern ) . '"'; + } + if ( '' !== $val_message ) { + $input .= ' title="' . esc_attr( $val_message ) . '"'; + } + if ( '' !== $default_val ) { + $input .= ' value="' . esc_attr( $default_val ) . '"'; + } + if ( '' !== $help_text ) { + $input .= ' aria-describedby="' . esc_attr( $field_id ) . '-help"'; + } + if ( $required ) { + $input .= ' aria-required="true"'; + } + $input .= ' data-field-type="text"/>'; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $input; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--text', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_text_field( $attributes, $content, $block ); diff --git a/src/blocks/form-text-field/save.js b/src/blocks/form-text-field/save.js new file mode 100644 index 0000000..1e0c075 --- /dev/null +++ b/src/blocks/form-text-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Text Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-text-field/style.scss b/src/blocks/form-text-field/style.scss new file mode 100644 index 0000000..c75b2e8 --- /dev/null +++ b/src/blocks/form-text-field/style.scss @@ -0,0 +1,141 @@ +/** + * Form Text Field Block - Styles + * + * Shared styles for form fields + * + * @since 1.0.0 + */ + +.airo-wp-form-field { + // Inherit CSS custom properties from form container + --airo-wp-field-label-color: var(--airo-wp-form-label-color, inherit); + --airo-wp-field-border-color: var(--airo-wp-form-border-color, #d1d5db); + --airo-wp-field-border-radius: var(--airo-wp-form-border-radius, 0.375rem); + --airo-wp-field-focus-color: var(--airo-wp-form-focus-color, #2563eb); + --airo-wp-field-error-color: var(--airo-wp-form-error-color, #dc2626); + + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1 1 100%; + min-width: 0; // Prevent flex items from overflowing + + // Label + &__label { + display: block; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.5; + color: var(--airo-wp-field-label-color); + margin: 0; + } + + // Required indicator + &__required { + color: var(--airo-wp-field-error-color); + margin-left: 0.25rem; + font-weight: 600; + } + + // Input field + &__input, + &__textarea, + &__select { + box-sizing: border-box; // Prevent overflow from padding/border + margin: 0; // Reset browser/theme defaults for flex alignment + width: 100%; + max-width: 100%; // Ensure it doesn't overflow container + padding: var(--airo-wp-form-input-padding, 0.75rem); + font-size: 1rem; + line-height: 1.5; + color: inherit; + background-color: var(--airo-wp-form-field-bg, var(--wp--preset--color--base, #fff)); + border: 1px solid var(--airo-wp-field-border-color); + border-radius: var(--airo-wp-field-border-radius); + transition: all 0.2s ease; + min-height: var(--airo-wp-form-input-height, 44px); // Touch-friendly + + &::placeholder { + color: #9ca3af; + opacity: 1; + } + + &:hover:not(:disabled) { + border-color: var(--airo-wp-field-focus-color); + } + + &:focus { + outline: none; + border-color: var(--airo-wp-field-focus-color); + } + + &:disabled { + cursor: not-allowed; + // Keep the field background color even when disabled (for editor preview) + // Only apply default gray if no background color is set + background-color: var( + --airo-wp-form-field-bg, + var(--wp--preset--color--base, #f3f4f6) + ); + } + + // Invalid state (only show after validation attempt) + &[aria-invalid="true"] { + border-color: var(--airo-wp-field-error-color); + } + } + + // Textarea specific + &__textarea { + resize: vertical; + min-height: 100px; + } + + // Help text + &__help { + font-size: 0.875rem; + line-height: 1.5; + color: #6b7280; + margin: 0; + } + + // Error message + &__error { + font-size: 0.875rem; + line-height: 1.5; + color: var(--airo-wp-field-error-color); + margin: 0; + display: none; + + &[aria-live] { + display: block; + } + } +} + +// Responsive adjustments +@media (max-width: 768px) { + + .airo-wp-form-field { + + &__input, + &__textarea, + &__select { + font-size: 16px; // Prevent zoom on iOS + } + } +} + +// Print styles +@media print { + + .airo-wp-form-field { + + &__input, + &__textarea, + &__select { + border: 1px solid #000; + background: none; + } + } +} diff --git a/src/blocks/form-textarea-field/block.json b/src/blocks/form-textarea-field/block.json new file mode 100644 index 0000000..12da526 --- /dev/null +++ b/src/blocks/form-textarea-field/block.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-textarea-field", + "version": "1.0.0", + "title": "Textarea", + "category": "airo-wp", + "description": "Multi-line text area for longer text input.", + "keywords": [ + "form", + "textarea", + "text", + "message", + "field" + ], + "textdomain": "airo-wp", + "icon": "edit-page", + "parent": [ + "airo-wp/form-builder" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Message" + }, + "placeholder": { + "type": "string", + "default": "" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "rows": { + "type": "number", + "default": 4 + }, + "maxLength": { + "type": "number", + "default": 0 + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "usesContext": [ + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/fieldSpacing", + "airo-wp/form/formId" + ], + "supports": { + "html": false, + "reusable": false, + "inserter": true + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-textarea-field/deprecated.js b/src/blocks/form-textarea-field/deprecated.js new file mode 100644 index 0000000..7302c0a --- /dev/null +++ b/src/blocks/form-textarea-field/deprecated.js @@ -0,0 +1,401 @@ +/** + * Form Textarea Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static + * textarea field so existing content — and the current block-patterns HTML — + * migrates silently (passthrough) with no "Attempt Recovery" warning. + * + * Also handles backward compatibility for blocks saved with the old name + * (airo-wp/form-textarea) before the rename to airo-wp/form-textarea-field. + * + * @since 2.5.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports (with border → __experimentalBorder if applicable). + */ +const sharedSupports = { + html: false, + reusable: false, + inserter: true, +}; + +/** + * Shared attribute schema for the static deprecations (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Message' }, + placeholder: { type: 'string', default: '' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + rows: { type: 'number', default: 4 }, + maxLength: { type: 'number', default: 0 }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static textarea field carries this wrapper class; the + // dynamic block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && + innerHTML.includes('airo-wp-form-field--textarea') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + rows, + maxLength, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--textarea' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + '; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $textarea; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--textarea', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_textarea_field( $attributes, $content, $block ); diff --git a/src/blocks/form-textarea-field/save.js b/src/blocks/form-textarea-field/save.js new file mode 100644 index 0000000..9e52e33 --- /dev/null +++ b/src/blocks/form-textarea-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Textarea Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-textarea-field/style.scss b/src/blocks/form-textarea-field/style.scss new file mode 100644 index 0000000..18103b6 --- /dev/null +++ b/src/blocks/form-textarea-field/style.scss @@ -0,0 +1,10 @@ +/** + * Form Textarea Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// Textarea uses the same styles as text field +@use '../form-text-field/style' as *; diff --git a/src/blocks/form-time-field/block.json b/src/blocks/form-time-field/block.json new file mode 100644 index 0000000..e2a7d89 --- /dev/null +++ b/src/blocks/form-time-field/block.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-time-field", + "version": "1.0.0", + "title": "Time Field", + "category": "airo-wp", + "description": "A time picker input field for forms.", + "keywords": [ + "form", + "time", + "clock", + "timepicker", + "hour" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "schedule", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "usesContext": [ + "airo-wp/form/fieldSpacing", + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/inputHeight", + "airo-wp/form/inputPadding" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Time" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "minTime": { + "type": "string", + "default": "" + }, + "maxTime": { + "type": "string", + "default": "" + }, + "step": { + "type": "number", + "default": 60 + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-time-field/deprecated.js b/src/blocks/form-time-field/deprecated.js new file mode 100644 index 0000000..2927fb5 --- /dev/null +++ b/src/blocks/form-time-field/deprecated.js @@ -0,0 +1,136 @@ +/** + * Form Time Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static time + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.5.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports. + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attribute schema for the static deprecation (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Time' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + minTime: { type: 'string', default: '' }, + maxTime: { type: 'string', default: '' }, + step: { type: 'number', default: 60 }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static time field carries this wrapper class; the dynamic + // block saves no inner HTML, so it never matches. + return ( + Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--time') + ); + }, + + save({ attributes }) { + const { + fieldName, + label, + helpText, + required, + defaultValue, + minTime, + maxTime, + step, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--time' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic]; diff --git a/src/blocks/form-time-field/edit.js b/src/blocks/form-time-field/edit.js new file mode 100644 index 0000000..e14883e --- /dev/null +++ b/src/blocks/form-time-field/edit.js @@ -0,0 +1,331 @@ +/** + * Form Time Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + ToggleControl, + SelectControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- NumberControl is stable in practice + __experimentalNumberControl as NumberControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormTimeFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + helpText, + required, + defaultValue, + minTime, + maxTime, + step, + fieldWidth, + } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `time-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames('airo-wp-form-field', 'airo-wp-form-field--time'); + + const fieldStyles = { + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Time', + helpText: '', + required: false, + defaultValue: '', + minTime: '', + maxTime: '', + step: 60, + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^time-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Time'} + onDeselect={() => setAttributes({ label: 'Time' })} + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + minTime !== ''} + onDeselect={() => setAttributes({ minTime: '' })} + isShownByDefault + > + + setAttributes({ minTime: value }) + } + help={__( + 'Format: HH:MM (e.g., 09:00)', + 'airo-wp' + )} + type="time" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + maxTime !== ''} + onDeselect={() => setAttributes({ maxTime: '' })} + isShownByDefault + > + + setAttributes({ maxTime: value }) + } + help={__( + 'Format: HH:MM (e.g., 17:00)', + 'airo-wp' + )} + type="time" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + step !== 60} + onDeselect={() => setAttributes({ step: 60 })} + isShownByDefault + > + + setAttributes({ step: parseInt(value) || 60 }) + } + help={__( + 'Time increment in seconds (60 = 1 minute)', + 'airo-wp' + )} + min={1} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-filled time (Format: HH:MM)', + 'airo-wp' + )} + type="time" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-time-field/editor.scss b/src/blocks/form-time-field/editor.scss new file mode 100644 index 0000000..8c27515 --- /dev/null +++ b/src/blocks/form-time-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form Time Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-time-field/index.js b/src/blocks/form-time-field/index.js new file mode 100644 index 0000000..124069e --- /dev/null +++ b/src/blocks/form-time-field/index.js @@ -0,0 +1,27 @@ +/** + * Form Time Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { scheduled as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-time-field/render.php b/src/blocks/form-time-field/render.php new file mode 100644 index 0000000..86aad01 --- /dev/null +++ b/src/blocks/form-time-field/render.php @@ -0,0 +1,79 @@ +'; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $input; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--time', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_time_field( $attributes, $content, $block ); diff --git a/src/blocks/form-time-field/save.js b/src/blocks/form-time-field/save.js new file mode 100644 index 0000000..345873c --- /dev/null +++ b/src/blocks/form-time-field/save.js @@ -0,0 +1,13 @@ +/** + * Form Time Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-time-field/style.scss b/src/blocks/form-time-field/style.scss new file mode 100644 index 0000000..8b26294 --- /dev/null +++ b/src/blocks/form-time-field/style.scss @@ -0,0 +1,10 @@ +/** + * Form Time Field Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// Time field uses the same styles as text field +@use '../form-text-field/style' as *; diff --git a/src/blocks/form-url-field/block.json b/src/blocks/form-url-field/block.json new file mode 100644 index 0000000..3ab6ccb --- /dev/null +++ b/src/blocks/form-url-field/block.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/form-url-field", + "version": "1.0.0", + "title": "URL Field", + "category": "airo-wp", + "description": "A URL/website address input field for forms.", + "keywords": [ + "form", + "url", + "website", + "link", + "address" + ], + "parent": [ + "airo-wp/form-builder" + ], + "textdomain": "airo-wp", + "icon": "admin-links", + "supports": { + "html": false, + "anchor": false, + "customClassName": false, + "reusable": false + }, + "usesContext": [ + "airo-wp/form/fieldSpacing", + "airo-wp/form/fieldLabelColor", + "airo-wp/form/fieldBorderColor", + "airo-wp/form/fieldBackgroundColor", + "airo-wp/form/inputHeight", + "airo-wp/form/inputPadding" + ], + "attributes": { + "fieldName": { + "type": "string", + "default": "" + }, + "label": { + "type": "string", + "default": "Website URL" + }, + "placeholder": { + "type": "string", + "default": "https://example.com" + }, + "helpText": { + "type": "string", + "default": "" + }, + "required": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "type": "string", + "default": "" + }, + "fieldWidth": { + "type": "string", + "default": "100" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php" +} diff --git a/src/blocks/form-url-field/deprecated.js b/src/blocks/form-url-field/deprecated.js new file mode 100644 index 0000000..3cb9054 --- /dev/null +++ b/src/blocks/form-url-field/deprecated.js @@ -0,0 +1,128 @@ +/** + * Form URL Field Block - Deprecated Versions + * + * vStatic reproduces the last STATIC save (the block is now server-rendered via + * render.php; save() returns null). isEligible matches any stored static URL + * field so existing content — and the current block-patterns HTML — migrates + * silently (passthrough) with no "Attempt Recovery" warning. + * + * @since 2.5.0 + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +/** + * Supports definition for deprecated versions. + * Matches block.json supports. + */ +const sharedSupports = { + html: false, + anchor: false, + customClassName: false, + reusable: false, +}; + +/** + * Shared attribute schema for the static deprecation (identical to block.json). + */ +const sharedAttributes = { + fieldName: { type: 'string', default: '' }, + label: { type: 'string', default: 'Website URL' }, + placeholder: { type: 'string', default: 'https://example.com' }, + helpText: { type: 'string', default: '' }, + required: { type: 'boolean', default: false }, + defaultValue: { type: 'string', default: '' }, + fieldWidth: { type: 'string', default: '100' }, +}; + +/** + * The last static markup, immediately before the block became server-rendered. + */ +const vStatic = { + supports: sharedSupports, + attributes: sharedAttributes, + + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // Any stored static URL field carries this wrapper class; the dynamic + // block saves no inner HTML, so it never matches. + return Boolean(innerHTML) && innerHTML.includes('airo-wp-form-field--url'); + }, + + save({ attributes }) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + fieldWidth, + } = attributes; + + const fieldClasses = classnames( + 'airo-wp-form-field', + 'airo-wp-form-field--url' + ); + + const blockProps = useBlockProps.save({ + className: fieldClasses, + style: { + // Use flex-basis with calc to account for gap between fields + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + ); + }, + + migrate(attributes) { + return attributes; + }, +}; + +export default [vStatic]; diff --git a/src/blocks/form-url-field/edit.js b/src/blocks/form-url-field/edit.js new file mode 100644 index 0000000..216323d --- /dev/null +++ b/src/blocks/form-url-field/edit.js @@ -0,0 +1,284 @@ +/** + * Form URL Field Block - Edit Component + * + * @since 1.0.0 + */ + +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { + TextControl, + ToggleControl, + SelectControl, +} from '@wordpress/components'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useEffect } from '@wordpress/element'; +import classnames from 'classnames'; +import { convertColorToCSSVar } from '../../utils/convert-preset-to-css-var'; + +const FIELD_WIDTH_OPTIONS = [ + { label: __('Full Width (100%)', 'airo-wp'), value: '100' }, + { label: __('Half Width (50%)', 'airo-wp'), value: '50' }, + { label: __('One Third (33%)', 'airo-wp'), value: '33' }, + { label: __('Two Thirds (66%)', 'airo-wp'), value: '66' }, + { label: __('One Quarter (25%)', 'airo-wp'), value: '25' }, + { label: __('Three Quarters (75%)', 'airo-wp'), value: '75' }, +]; + +export default function FormURLFieldEdit({ + attributes, + setAttributes, + clientId, + context, +}) { + const { + fieldName, + label, + placeholder, + helpText, + required, + defaultValue, + fieldWidth, + } = attributes; + + // Generate field name from clientId if empty + useEffect(() => { + if (!fieldName) { + setAttributes({ fieldName: `url-${clientId.slice(0, 8)}` }); + } + }, [fieldName, clientId, setAttributes]); + + // Get context values from parent form + const fieldBackgroundColor = + context['airo-wp/form/fieldBackgroundColor']; + + const fieldClasses = classnames('airo-wp-form-field', 'airo-wp-form-field--url'); + + const fieldStyles = { + '--airo-wp-form-field-bg': convertColorToCSSVar(fieldBackgroundColor), + }; + + const blockProps = useBlockProps({ + className: fieldClasses, + style: { + ...fieldStyles, + flexBasis: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + maxWidth: + fieldWidth === '100' + ? '100%' + : `calc(${fieldWidth}% - var(--airo-wp-form-field-spacing, 1.5rem) / 2)`, + }, + }); + + const fieldId = `field-${fieldName}`; + + return ( + <> + + + setAttributes({ + fieldName: '', + label: 'Website URL', + placeholder: 'https://example.com', + helpText: '', + required: false, + defaultValue: '', + fieldWidth: '100', + }) + } + > + + !!fieldName && + !/^url-[a-z0-9]{1,8}$/i.test(fieldName) + } + onDeselect={() => setAttributes({ fieldName: '' })} + isShownByDefault + > + + setAttributes({ + fieldName: value.replace( + /[^a-z0-9_-]/gi, + '' + ), + }) + } + help={__( + 'Unique identifier for this field (letters, numbers, hyphens, underscores only)', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + label !== 'Website URL'} + onDeselect={() => + setAttributes({ label: 'Website URL' }) + } + isShownByDefault + > + + setAttributes({ label: value }) + } + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + required !== false} + onDeselect={() => setAttributes({ required: false })} + isShownByDefault + > + + setAttributes({ required: value }) + } + __nextHasNoMarginBottom + /> + + + placeholder !== 'https://example.com'} + onDeselect={() => + setAttributes({ + placeholder: 'https://example.com', + }) + } + isShownByDefault + > + + setAttributes({ placeholder: value }) + } + help={__( + 'Example text shown when field is empty', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + helpText !== ''} + onDeselect={() => setAttributes({ helpText: '' })} + isShownByDefault + > + + setAttributes({ helpText: value }) + } + help={__( + 'Additional guidance shown below the field', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + defaultValue !== ''} + onDeselect={() => setAttributes({ defaultValue: '' })} + isShownByDefault + > + + setAttributes({ defaultValue: value }) + } + help={__( + 'Pre-filled value for this field', + 'airo-wp' + )} + type="url" + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + fieldWidth !== '100'} + onDeselect={() => setAttributes({ fieldWidth: '100' })} + isShownByDefault + > + + setAttributes({ fieldWidth: value }) + } + help={__( + 'Set field width to create multi-column layouts', + 'airo-wp' + )} + __next40pxDefaultSize + __nextHasNoMarginBottom + /> + + + + +
    + + + + + {helpText && ( +

    + {helpText} +

    + )} +
    + + ); +} diff --git a/src/blocks/form-url-field/editor.scss b/src/blocks/form-url-field/editor.scss new file mode 100644 index 0000000..d24d351 --- /dev/null +++ b/src/blocks/form-url-field/editor.scss @@ -0,0 +1,8 @@ +/** + * Form URL Field Block - Editor Styles + * + * @since 1.0.0 + */ + +// Import the main frontend styles +@import './style'; diff --git a/src/blocks/form-url-field/index.js b/src/blocks/form-url-field/index.js new file mode 100644 index 0000000..3352790 --- /dev/null +++ b/src/blocks/form-url-field/index.js @@ -0,0 +1,27 @@ +/** + * Form URL Field Block - Registration + * + * @since 1.0.0 + */ + +import { registerBlockType } from '@wordpress/blocks'; +import { link as icon } from '@wordpress/icons'; + +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import deprecated from './deprecated'; +import { ICON_COLOR } from '../shared/constants'; + +import './style.scss'; +import './editor.scss'; + +registerBlockType(metadata.name, { + icon: { + src: icon, + foreground: ICON_COLOR, + }, + edit: Edit, + save: Save, + deprecated, +}); diff --git a/src/blocks/form-url-field/render.php b/src/blocks/form-url-field/render.php new file mode 100644 index 0000000..0d5ce9b --- /dev/null +++ b/src/blocks/form-url-field/render.php @@ -0,0 +1,73 @@ +'; + + $inner = airowp_form_field_label_html( $field_id, $label, $required ); + $inner .= $input; + $inner .= airowp_form_field_help_html( $field_id, $help_text ); + + $wrapper = get_block_wrapper_attributes( + array( + 'class' => 'airo-wp-form-field airo-wp-form-field--url', + 'style' => airowp_form_field_width_style( $field_width ), + ) + ); + + echo '
    ' . $inner . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +airowp_render_form_url_field( $attributes, $content, $block ); diff --git a/src/blocks/form-url-field/save.js b/src/blocks/form-url-field/save.js new file mode 100644 index 0000000..77b38b7 --- /dev/null +++ b/src/blocks/form-url-field/save.js @@ -0,0 +1,13 @@ +/** + * Form URL Field Block - Save Component + * + * Dynamic block rendered server-side via render.php. No static HTML is saved, + * so the serialized block is a single self-closing comment and authored / + * translated / pattern-substituted field text can never trip block validation. + * Historical static markup is migrated by ./deprecated.js. + * + * @return {null} Null because this is a dynamic block. + */ +export default function save() { + return null; +} diff --git a/src/blocks/form-url-field/style.scss b/src/blocks/form-url-field/style.scss new file mode 100644 index 0000000..bf3ad8a --- /dev/null +++ b/src/blocks/form-url-field/style.scss @@ -0,0 +1,10 @@ +/** + * Form URL Field Block - Styles + * + * Uses shared form field styles from text field + * + * @since 1.0.0 + */ + +// URL field uses the same styles as text field +@use '../form-text-field/style' as *; diff --git a/src/blocks/grid/block.json b/src/blocks/grid/block.json new file mode 100644 index 0000000..ab5f331 --- /dev/null +++ b/src/blocks/grid/block.json @@ -0,0 +1,256 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "airo-wp/grid", + "version": "1.0.0", + "title": "Grid", + "category": "airo-wp", + "description": "Responsive multi-column grid layouts. Perfect for feature grids, product listings, team members, and card layouts.", + "keywords": [ + "grid", + "columns", + "layout", + "container", + "responsive" + ], + "textdomain": "airo-wp", + "icon": "grid-view", + "supports": { + "anchor": true, + "align": [ + "wide", + "full" + ], + "html": false, + "inserter": true, + "layout": { + "allowSwitching": false, + "allowInheriting": false, + "allowEditing": false, + "allowSizingOnChildren": true, + "default": { + "type": "default" + } + }, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "padding": true, + "blockGap": true + } + }, + "dimensions": { + "minHeight": true + }, + "color": { + "background": true, + "text": true, + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "background": { + "backgroundImage": true, + "backgroundSize": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "shadow": true, + "position": { + "sticky": true + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": true, + "style": true, + "width": true + } + } + }, + "attributes": { + "align": { + "type": "string", + "default": "full" + }, + "tagName": { + "type": "string", + "default": "div" + }, + "desktopColumns": { + "type": "number", + "default": 3 + }, + "tabletColumns": { + "type": "number", + "default": 2 + }, + "mobileColumns": { + "type": "number", + "default": 1 + }, + "rowGap": { + "type": "string", + "default": "" + }, + "columnGap": { + "type": "string", + "default": "" + }, + "style": { + "type": "object", + "default": { + "spacing": { + "blockGap": "var:preset|spacing|50", + "padding": { + "top": "var:preset|spacing|50", + "bottom": "var:preset|spacing|50", + "left": "var:preset|spacing|30", + "right": "var:preset|spacing|30" + } + } + } + }, + "alignItems": { + "type": "string", + "default": "stretch" + }, + "textAlign": { + "type": "string" + }, + "hoverBackgroundColor": { + "type": "string", + "default": "" + }, + "hoverTextColor": { + "type": "string", + "default": "" + }, + "hoverIconBackgroundColor": { + "type": "string", + "default": "" + }, + "hoverButtonBackgroundColor": { + "type": "string", + "default": "" + }, + "overlayColor": { + "type": "string", + "default": "" + }, + "constrainWidth": { + "type": "boolean", + "default": false + }, + "contentWidth": { + "type": "string", + "default": "" + }, + "columnMinWidth": { + "type": "string", + "default": "" + } + }, + "providesContext": { + "airo-wp/alignItems": "alignItems", + "airo-wp/textAlign": "textAlign", + "airo-wp/hoverIconBackgroundColor": "hoverIconBackgroundColor", + "airo-wp/hoverButtonBackgroundColor": "hoverButtonBackgroundColor" + }, + "example": { + "attributes": { + "desktopColumns": 3, + "tabletColumns": 2, + "mobileColumns": 1 + }, + "innerBlocks": [ + { + "name": "core/group", + "attributes": { + "style": { + "spacing": { + "padding": "1rem" + }, + "border": { + "width": "1px", + "style": "solid" + } + } + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "level": 3, + "content": "Feature 1" + } + } + ] + }, + { + "name": "core/group", + "attributes": { + "style": { + "spacing": { + "padding": "1rem" + }, + "border": { + "width": "1px", + "style": "solid" + } + } + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "level": 3, + "content": "Feature 2" + } + } + ] + }, + { + "name": "core/group", + "attributes": { + "style": { + "spacing": { + "padding": "1rem" + }, + "border": { + "width": "1px", + "style": "solid" + } + } + }, + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "level": 3, + "content": "Feature 3" + } + } + ] + } + ] + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "viewScript": "file:./view.js" +} diff --git a/src/blocks/grid/deprecated.js b/src/blocks/grid/deprecated.js new file mode 100644 index 0000000..cd8687d --- /dev/null +++ b/src/blocks/grid/deprecated.js @@ -0,0 +1,518 @@ +/** + * Grid Block - Deprecated versions + * + * @since 1.0.0 + */ + +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; +import { + convertPresetToCSSVar, + convertColorToCSSVar, +} from '../../utils/convert-preset-to-css-var'; +import { + hasOverlayStyleClass, + hoverVariationClasses, +} from '../../utils/style-variation-classes'; +import metadata from './block.json'; +import { getDeprecatedBlockHTML } from '../../utils/deprecated-block-html'; + +// Captures the column min width from a `minmax(, 1fr)` grid track. +const MIN_WIDTH_RE = /minmax\(\s*(\d+(?:\.\d+)?[a-z%]+)\s*,\s*1fr\s*\)/i; + +const sharedSupports = { + anchor: true, + align: ['wide', 'full'], + html: false, + inserter: true, + layout: { + allowSwitching: false, + allowInheriting: false, + allowEditing: false, + allowSizingOnChildren: true, + allowContentEditing: false, + default: { + type: 'constrained', + }, + }, + spacing: { + margin: true, + padding: true, + blockGap: true, + __experimentalDefaultControls: { + padding: true, + blockGap: true, + }, + }, + dimensions: { + minHeight: true, + }, + color: { + background: true, + text: true, + gradients: true, + link: true, + __experimentalDefaultControls: { + background: true, + text: true, + }, + }, + background: { + backgroundImage: true, + backgroundSize: true, + }, + typography: { + fontSize: true, + lineHeight: true, + __experimentalDefaultControls: { + fontSize: true, + }, + }, + shadow: true, + position: { + sticky: true, + }, + __experimentalBorder: { + color: true, + radius: true, + style: true, + width: true, + __experimentalDefaultControls: { + color: true, + radius: true, + style: true, + width: true, + }, + }, +}; + +// Before style-kit overlay/hover variation detection (and, for overlay, +// before the `overlayColor` attribute existed at all). The current save() +// emits `airo-wp-grid--has-overlay` when a style-kit overlay variation +// (`is-style-overlay-*`) is present on className (or `overlayColor` is set), +// and emits `airo-wp-grid--has-hover-{text,icon,button}` activation classes for +// the matching `is-style-hover-*` variation families — mirroring Section's +// behavior. Grids saved with such a variation but no matching class in their +// stored HTML fail validation against the new save(). +// +// Note the asymmetry with Row's equivalent deprecation: the overlay branch +// here can ONLY ever be reached via a className variation, never via +// `overlayColor` — that attribute didn't exist on Grid before this change, so +// no stored Grid content could have set it. That's intentional, not a gap to +// "fix" for symmetry. +// +// isEligible targets exactly that signature (a variation on className with +// no matching class in the stored HTML) so those grids migrate SILENTLY. +// save() reproduces this file's pre-change output (no overlay logic at all, +// no hover activation classes) so it also byte-matches on WP versions that +// still validate the deprecation's save() before migrating. migrate() is a +// passthrough — only the serialised class differs, not the attribute +// values; the current save() then re-renders with the classes derived from +// the variation (and, for overlay, the new overlayColor attribute default +// of ''). +const styleVariationClasses = { + supports: metadata.supports, + attributes: { ...metadata.attributes }, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + if (!innerHTML || !innerHTML.includes('airo-wp-grid')) { + return false; + } + + const overlayMismatch = + hasOverlayStyleClass(attributes.className) && + !innerHTML.includes('airo-wp-grid--has-overlay'); + + const hoverMismatch = hoverVariationClasses( + attributes.className, + 'airo-wp-grid' + ).some((activationClass) => !innerHTML.includes(activationClass)); + + return overlayMismatch || hoverMismatch; + }, + save({ attributes }) { + const { + tagName = 'div', + constrainWidth, + contentWidth, + desktopColumns, + tabletColumns, + mobileColumns, + rowGap, + columnGap, + alignItems, + columnMinWidth, + hoverBackgroundColor, + hoverTextColor, + hoverIconBackgroundColor, + hoverButtonBackgroundColor, + style, + } = attributes; + + // Pre-change className: no overlay support at all, no hover + // activation classes. + const className = [ + 'airo-wp-grid', + `airo-wp-grid-cols-${desktopColumns}`, + `airo-wp-grid-cols-tablet-${tabletColumns}`, + `airo-wp-grid-cols-mobile-${mobileColumns}`, + !constrainWidth && 'airo-wp-no-width-constraint', + ] + .filter(Boolean) + .join(' '); + + const TagName = tagName || 'div'; + const blockProps = useBlockProps.save({ + className, + style: { + ...(hoverBackgroundColor && { + '--airo-wp-hover-bg-color': + convertColorToCSSVar(hoverBackgroundColor), + }), + ...(hoverTextColor && { + '--airo-wp-hover-text-color': + convertColorToCSSVar(hoverTextColor), + }), + ...(hoverIconBackgroundColor && { + '--airo-wp-parent-hover-icon-bg': convertColorToCSSVar( + hoverIconBackgroundColor + ), + }), + ...(hoverButtonBackgroundColor && { + '--airo-wp-parent-hover-button-bg': convertColorToCSSVar( + hoverButtonBackgroundColor + ), + }), + }, + }); + + const blockGapValue = style?.spacing?.blockGap; + const isBlockGapObject = + typeof blockGapValue === 'object' && blockGapValue !== null; + const blockGapRow = convertPresetToCSSVar( + isBlockGapObject ? blockGapValue?.top : blockGapValue + ); + const blockGapColumn = convertPresetToCSSVar( + isBlockGapObject ? blockGapValue?.left : blockGapValue + ); + const defaultGap = 'var(--wp--preset--spacing--50)'; + + const innerStyles = { + display: 'grid', + gridTemplateColumns: columnMinWidth + ? `repeat(${desktopColumns || 3}, minmax(${columnMinWidth}, 1fr))` + : `repeat(${desktopColumns || 3}, 1fr)`, + alignItems: alignItems || 'stretch', + rowGap: blockGapRow || rowGap || defaultGap, + columnGap: blockGapColumn || columnGap || defaultGap, + }; + + if (constrainWidth) { + innerStyles.maxWidth = + contentWidth || + 'var(--wp--style--global--content-size, 1140px)'; + innerStyles.marginLeft = 'auto'; + innerStyles.marginRight = 'auto'; + } + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-grid__inner', + style: innerStyles, + }); + + return ( + +
    + + ); + }, + migrate(attributes) { + // Only the serialised class differs; the current save() derives it + // from the style variation (and the new overlayColor attribute, + // which defaults to '' for this old content) so no attribute change. + return attributes; + }, +}; + +// Version 1: Before align attribute - used className for alignment +const v1 = { + supports: sharedSupports, + attributes: { + // Old blocks don't have align attribute, only className + style: { + type: 'object', + }, + layout: { + type: 'object', + }, + contentWidth: { + type: 'string', + }, + hoverBackgroundColor: { + type: 'string', + }, + hoverTextColor: { + type: 'string', + }, + hoverIconBackgroundColor: { + type: 'string', + }, + hoverButtonBackgroundColor: { + type: 'string', + }, + }, + // No isEligible: markup-change deprecation, reached by save-matching on an + // INVALID block (WordPress skips isEligible for those). The old guard, + // `attributes.align === undefined`, matched nearly every CURRENT grid too: + // `align` has no default, so it is absent from the raw comment attributes on + // any grid the author never aligned wide/full. + save({ attributes }) { + const { + hoverBackgroundColor, + hoverTextColor, + hoverIconBackgroundColor, + hoverButtonBackgroundColor, + layout, + contentWidth, + } = attributes; + + let contentSize; + if (layout && 'contentSize' in layout) { + contentSize = layout.contentSize; + } else { + contentSize = contentWidth || '1200px'; + } + + const className = [ + 'airo-wp-stack', + !contentSize && 'airo-wp-no-width-constraint', + ] + .filter(Boolean) + .join(' '); + + const blockProps = useBlockProps.save({ + className, + style: { + ...(hoverBackgroundColor && { + '--airo-wp-hover-bg-color': + convertPresetToCSSVar(hoverBackgroundColor), + }), + ...(hoverTextColor && { + '--airo-wp-hover-text-color': + convertPresetToCSSVar(hoverTextColor), + }), + ...(hoverIconBackgroundColor && { + '--airo-wp-parent-hover-icon-bg': convertPresetToCSSVar( + hoverIconBackgroundColor + ), + }), + ...(hoverButtonBackgroundColor && { + '--airo-wp-parent-hover-button-bg': convertPresetToCSSVar( + hoverButtonBackgroundColor + ), + }), + }, + }); + + const innerStyle = {}; + if (contentSize) { + innerStyle.maxWidth = contentSize; + innerStyle.marginLeft = 'auto'; + innerStyle.marginRight = 'auto'; + } + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-stack__inner', + style: innerStyle, + }); + + return ( +
    +
    +
    + ); + }, + migrate(oldAttributes) { + // Extract align from className if it exists + const className = oldAttributes.className || ''; + let align; + + if (className.includes('alignfull')) { + align = 'full'; + } else if (className.includes('alignwide')) { + align = 'wide'; + } + + // Remove align classes from className since they'll be auto-added by WordPress + const cleanClassName = className + .split(' ') + .filter((cls) => cls !== 'alignfull' && cls !== 'alignwide') + .join(' ') + .trim(); + + // Return migrated attributes + return { + ...oldAttributes, + align, + className: cleanClassName || undefined, + }; + }, +}; + +/** + * Legacy responsive-grid markup from gd-pattern-library patterns. + * + * AI-generated patterns hard-coded `grid-template-columns: repeat(N, minmax( + * , 1fr))` directly in the inner div's INLINE style, with no + * columnMinWidth block attribute. The width lived only in CSS, so the current + * save() (which reads columnMinWidth) can't reproduce it and the block fails + * validation ("Attempt Recovery"). This deprecation captures the stored inner + * style, reproduces its grid-template-columns verbatim so validation passes, + * and migrate() recovers the width into the columnMinWidth attribute — after + * which the current save() reproduces the markup from the attribute as normal. + */ +const legacyMinWidth = { + supports: sharedSupports, + attributes: { + ...metadata.attributes, + legacyInnerStyle: { + type: 'string', + source: 'attribute', + selector: '.airo-wp-grid__inner', + attribute: 'style', + }, + }, + isEligible(attributes, innerBlocks, extra) { + const innerHTML = getDeprecatedBlockHTML(extra); + // A minmax() track alone does NOT mean "legacy": the current save() emits + // one too, as soon as columnMinWidth is set. What marks the old + // AI-generated pattern content is the track being baked into the HTML with + // no columnMinWidth in the block comment — and migrate() below recovers it + // from the markup. A current grid that renders minmax() always carries the + // attribute (it is non-default, so WordPress serializes it), so requiring + // its absence excludes current content without missing any legacy content. + return ( + !!innerHTML && + !attributes.columnMinWidth && + /grid-template-columns:\s*repeat\([^)]*minmax/i.test(innerHTML) + ); + }, + save({ attributes }) { + const { + tagName = 'div', + constrainWidth, + contentWidth, + desktopColumns, + tabletColumns, + mobileColumns, + rowGap, + columnGap, + alignItems, + hoverBackgroundColor, + hoverTextColor, + hoverIconBackgroundColor, + hoverButtonBackgroundColor, + style, + legacyInnerStyle, + } = attributes; + + const className = [ + 'airo-wp-grid', + `airo-wp-grid-cols-${desktopColumns}`, + `airo-wp-grid-cols-tablet-${tabletColumns}`, + `airo-wp-grid-cols-mobile-${mobileColumns}`, + !constrainWidth && 'airo-wp-no-width-constraint', + ] + .filter(Boolean) + .join(' '); + + const TagName = tagName || 'div'; + const blockProps = useBlockProps.save({ + className, + style: { + ...(hoverBackgroundColor && { + '--airo-wp-hover-bg-color': + convertColorToCSSVar(hoverBackgroundColor), + }), + ...(hoverTextColor && { + '--airo-wp-hover-text-color': + convertColorToCSSVar(hoverTextColor), + }), + ...(hoverIconBackgroundColor && { + '--airo-wp-parent-hover-icon-bg': convertColorToCSSVar( + hoverIconBackgroundColor + ), + }), + ...(hoverButtonBackgroundColor && { + '--airo-wp-parent-hover-button-bg': convertColorToCSSVar( + hoverButtonBackgroundColor + ), + }), + }, + }); + + const blockGapValue = style?.spacing?.blockGap; + const isBlockGapObject = + typeof blockGapValue === 'object' && blockGapValue !== null; + const blockGapRow = convertPresetToCSSVar( + isBlockGapObject ? blockGapValue?.top : blockGapValue + ); + const blockGapColumn = convertPresetToCSSVar( + isBlockGapObject ? blockGapValue?.left : blockGapValue + ); + const defaultGap = 'var(--wp--preset--spacing--50)'; + + // Reproduce the stored grid-template-columns verbatim from the captured + // inline style so this matches byte-for-byte. + const gtc = (legacyInnerStyle || '').match( + /grid-template-columns:\s*([^;]+)/i + ); + + const innerStyles = { + display: 'grid', + gridTemplateColumns: gtc + ? gtc[1].trim() + : `repeat(${desktopColumns || 3}, 1fr)`, + alignItems: alignItems || 'stretch', + rowGap: blockGapRow || rowGap || defaultGap, + columnGap: blockGapColumn || columnGap || defaultGap, + }; + + if (constrainWidth) { + innerStyles.maxWidth = + contentWidth || + 'var(--wp--style--global--content-size, 1140px)'; + innerStyles.marginLeft = 'auto'; + innerStyles.marginRight = 'auto'; + } + + const innerBlocksProps = useInnerBlocksProps.save({ + className: 'airo-wp-grid__inner', + style: innerStyles, + }); + + return ( + +
    + + ); + }, + migrate(attributes) { + const { legacyInnerStyle, ...rest } = attributes; + const gtc = (legacyInnerStyle || '').match( + /grid-template-columns:\s*([^;]+)/i + ); + const mm = gtc ? gtc[1].match(MIN_WIDTH_RE) : null; + return { + ...rest, + columnMinWidth: mm ? mm[1] : '', + }; + }, +}; + +// legacyMinWidth must come before styleVariationClasses: a legacy grid can +// match BOTH isEligible checks (a minmax(...) inline style AND a style-kit +// variation class), and only legacyMinWidth's migrate() recovers the +// columnMinWidth attribute from stored HTML. styleVariationClasses.migrate() +// is a passthrough, so if it "won" for such content, columnMinWidth would be +// silently dropped (columns collapse to 1fr) with no recovery warning. +export default [legacyMinWidth, styleVariationClasses, v1]; diff --git a/src/blocks/grid/edit.js b/src/blocks/grid/edit.js new file mode 100644 index 0000000..6d7e78d --- /dev/null +++ b/src/blocks/grid/edit.js @@ -0,0 +1,765 @@ +/** + * DSG Grid Block - Edit Component + * + * Responsive multi-column grid layouts. + * + * @since 1.0.0 + */ + +import { __, _n, sprintf } from '@wordpress/i18n'; +import { + useBlockProps, + useInnerBlocksProps, + InnerBlocks, + InspectorControls, + BlockControls, + AlignmentControl, + store as blockEditorStore, + useSettings, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; +import { + RangeControl, + SelectControl, + ToggleControl, + ToolbarGroup, + ToolbarDropdownMenu, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUnitControl as UnitControl, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseCustomUnits as useCustomUnits, +} from '@wordpress/components'; +import { grid as gridIcon } from '@wordpress/icons'; +import { DsgoInspectorPanel } from '../../components/shared'; +import { useState, useEffect } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { + convertPresetToCSSVar, + convertColorToCSSVar, +} from '../../utils/convert-preset-to-css-var'; +import { + encodeColorValue, + decodeColorValue, +} from '../../utils/encode-color-value'; +import { + hasOverlayStyleClass, + hoverVariationClasses, +} from '../../utils/style-variation-classes'; + +/** + * Grid Container Edit Component + * + * @param {Object} props Component props + * @param {Object} props.attributes Block attributes + * @param {Function} props.setAttributes Function to update attributes + * @param {string} props.clientId Block client ID + * @return {JSX.Element} Edit component + */ +export default function GridEdit({ attributes, setAttributes, clientId }) { + const { + align, + className, + tagName = 'div', + constrainWidth, + contentWidth, + columnMinWidth, + desktopColumns, + tabletColumns, + mobileColumns, + rowGap, + columnGap, + alignItems, + textAlign, + overlayColor, + hoverBackgroundColor, + hoverTextColor, + hoverIconBackgroundColor, + hoverButtonBackgroundColor, + style, + } = attributes; + + // Auto-migrate old blocks that use className for alignment + useEffect(() => { + if (!align && className) { + let newAlign; + if (className.includes('alignfull')) { + newAlign = 'full'; + } else if (className.includes('alignwide')) { + newAlign = 'wide'; + } + + if (newAlign) { + const cleanClassName = className + .split(' ') + .filter((cls) => cls !== 'alignfull' && cls !== 'alignwide') + .join(' ') + .trim(); + + setAttributes({ + align: newAlign, + className: cleanClassName || undefined, + }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Run only once on mount + + // Get theme settings (WP 6.5+) + const [themeContentSize] = useSettings('layout.contentSize'); + + // Get theme color palette and gradient settings + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + + // Get available spacing units + const units = useCustomUnits({ + availableUnits: ['px', 'em', 'rem', 'vh', 'vw', '%'], + }); + + const [useCustomGaps, setUseCustomGaps] = useState(!!(rowGap || columnGap)); + + // Get inner blocks to determine if container is empty + const hasInnerBlocks = useSelect( + (select) => { + const { getBlock } = select(blockEditorStore); + const block = getBlock(clientId); + return block?.innerBlocks?.length > 0; + }, + [clientId] + ); + + // Block wrapper props - outer div stays full width (must match save.js EXACTLY) + const hasOverlay = !!overlayColor || hasOverlayStyleClass(className); + const TagName = tagName || 'div'; + const blockProps = useBlockProps({ + className: [ + 'airo-wp-grid', + `airo-wp-grid-cols-${desktopColumns}`, + `airo-wp-grid-cols-tablet-${tabletColumns}`, + `airo-wp-grid-cols-mobile-${mobileColumns}`, + hasOverlay && 'airo-wp-grid--has-overlay', + ...hoverVariationClasses(className, 'airo-wp-grid'), + ] + .filter(Boolean) + .join(' '), + style: { + ...(hoverBackgroundColor && { + '--airo-wp-hover-bg-color': + convertColorToCSSVar(hoverBackgroundColor), + }), + ...(hoverTextColor && { + '--airo-wp-hover-text-color': convertColorToCSSVar(hoverTextColor), + }), + ...(hoverIconBackgroundColor && { + '--airo-wp-parent-hover-icon-bg': convertColorToCSSVar( + hoverIconBackgroundColor + ), + }), + ...(hoverButtonBackgroundColor && { + '--airo-wp-parent-hover-button-bg': convertColorToCSSVar( + hoverButtonBackgroundColor + ), + }), + ...(overlayColor && { + '--airo-wp-overlay-color': convertColorToCSSVar(overlayColor), + '--airo-wp-overlay-opacity': '0.8', + }), + }, + }); + + // Calculate inner styles declaratively (must match save.js EXACTLY) + // IMPORTANT: Always provide a default gap to prevent overlapping items + // Priority: blockGap (WordPress spacing) → custom rowGap/columnGap → preset fallback + // WordPress 6.1+ stores blockGap as object {top, left} for separate row/column gaps + // Also need to convert preset format (var:preset|spacing|X) to CSS variable + const blockGapValue = style?.spacing?.blockGap; + const isBlockGapObject = + typeof blockGapValue === 'object' && blockGapValue !== null; + const blockGapRow = convertPresetToCSSVar( + isBlockGapObject ? blockGapValue?.top : blockGapValue + ); + const blockGapColumn = convertPresetToCSSVar( + isBlockGapObject ? blockGapValue?.left : blockGapValue + ); + const defaultGap = 'var(--wp--preset--spacing--50)'; + + const innerStyles = { + display: 'grid', + gridTemplateColumns: columnMinWidth + ? `repeat(${desktopColumns || 3}, minmax(${columnMinWidth}, 1fr))` + : `repeat(${desktopColumns || 3}, 1fr)`, + alignItems: alignItems || 'stretch', + rowGap: blockGapRow || rowGap || defaultGap, + columnGap: blockGapColumn || columnGap || defaultGap, + }; + + // Apply width constraints if enabled + // Use custom contentWidth if set, otherwise fallback to theme's contentSize + if (constrainWidth) { + innerStyles.maxWidth = contentWidth || themeContentSize || '1140px'; + innerStyles.marginLeft = 'auto'; + innerStyles.marginRight = 'auto'; + } + + // Merge inner blocks props with inner styles + const innerBlocksProps = useInnerBlocksProps( + { + className: 'airo-wp-grid__inner', + style: innerStyles, + }, + { + templateLock: false, + renderAppender: hasInnerBlocks + ? undefined + : InnerBlocks.ButtonBlockAppender, + } + ); + + return ( + <> + + + i + 1 + ).map((count) => ({ + title: sprintf( + /* translators: %d: number of columns */ + _n( + '%d Column', + '%d Columns', + count, + 'airo-wp' + ), + count + ), + isActive: desktopColumns === count, + onClick: () => { + const newTabletCols = Math.min( + tabletColumns, + count + ); + const newMobileCols = Math.min( + mobileColumns, + newTabletCols + ); + setAttributes({ + desktopColumns: count, + ...(newTabletCols !== tabletColumns && { + tabletColumns: newTabletCols, + }), + ...(newMobileCols !== mobileColumns && { + mobileColumns: newMobileCols, + }), + }); + }, + }))} + /> + + + setAttributes({ textAlign: newAlign }) + } + /> + + + + )', 'airo-wp'), + value: 'div', + }, + { label: '
    ', value: 'section' }, + { label: '
    ', value: 'article' }, + { label: '